this란?
this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수이다. this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.
JavaScript에서 this는?
JavaScript에서 this는 함수를 호출할 때 어떻게 호출했느냐에 따라 동적으로 결정이 된다.
여기서 핵심은 ‘어떻게’ 호출했느냐이다. 메서드를 통해 호출했다면 메서드 앞에 있는 객체를, 일반 함수로 호출했다면 전역 객체를 가리킨다.
함수를 메서드로 호출할 경우
함수를 메서드로 호출할 경우에는 앞에 있는 객체를 가리킨다. 하지만 항상 그럴까? 예시를 통해 살펴보자.
person.hello();
hello함수는 person객체의 메서드로써 호출이 되었다. 여기서 person1은 메서드를 가지고 있기 때문에 리터럴 객체 혹은 인스턴스라는 것을 유추할 수 있다. person을 정의해보자.
- 인스턴스의 경우
- 클래스의 인스턴스의 경우
class Person {
hello() {
return this;
}
}
const person = new Person();
console.log(person.hello() === person, person instanceof Person); // true, true
Person 클래스의 인스턴스에서 hello메서드를 호출했을 때의 this는 Person 클래스의 인스턴스이다.
2. 생성자 함수의 인스턴스의 경우
function Person() {
this.hello = function () {
return this;
};
}
const person = new Person();
console.log(person.hello() === person, person instanceof Person); // true, true
Person 생성자 함수의 인스턴스에서 hello메서드를 호출했을 때의 this 또한 Person 생성자 함수의 인스턴스이다.
- 객체 리터럴의 경우
const person = {
hello: function () {
return this;
},
};
console.log(person.hello() === person); // true
person 객체 리터럴의 hello메서드를 호출했을 때의 this는 person객체이다.
정리하면, 클래스의 인스턴스 혹은 생성자 함수의 인스턴스이든 객체 리터럴이든 앞의 객체에 .을 붙여서 호출된 메서드 내에서 this는 앞의 객체를 가리킨다는 것을 알 수 있다.
위의 경우 모두 일반 함수로 선언한 메서드들이다. 그렇다면 일반 함수가 아니라 화살표 함수로 선언했다면 상황이 달라질까?
- 인스턴스의 경우
- 클래스의 인스턴스의 경우
class Person {
hello = () => {
return this;
};
}
const person = new Person();
console.log(person.hello() === person, person instanceof Person); // true, true
Person 클래스의 인스턴스에서 hello메서드를 호출했을 때의 this는 Person 클래스의 인스턴스이다.
2. 생성자 함수의 인스턴스의 경우
function Person() {
this.hello = () => {
return this;
};
}
const person = new Person();
console.log(person.hello() === person, person instanceof Person); // true, true
생성자 함수의 인스턴스에서 hello메서드를 호출했을 때의 this 또한 Person 생성자 함수의 인스턴스이다.
- 객체 리터럴의 경우
const person = {
hello: () => {
console.log(this); // {}
return this;
},
};
console.log(person.hello() === person); // false
클래스 및 생성자 함수 인스턴스의 메서드에서는 this가 여전히 인스턴스이다. 하지만 객체 리터럴의 메서드에서 this는 person이 아니다. 전역 객체를 가리키고 있다.(JavaScript 런타임 환경이 Node.js이고, Node.js는 CommonJS 모듈 시스템을 따르기에 각 파일은 함수로 감싸져있으며 이는 빈 객체 {}이다)
1번(인스턴스)부터 살펴보자. 클래스 내에 화살표 함수로 메서드를 선언하면 이는 인스턴스 프로퍼티로 정의된다. 이는 생성자 함수 내부에서 할당되는 것과 같다. 즉 다음과 같은 코드로 변환된다.
class Person {
constructor() {
this.hello = () => {
return this;
};
}
}
따라서 화살표 함수 hello는 생성자 실행 시점에 정의되며 이 때의 this는 새로 생성된 인스턴스이다. 화살표 함수는 이 this를 캡처해 고정시킨다.
이제 2번(객체 리터럴)을 살펴보자. 객체 리터럴은 새로운 스코프를 생성하지 않는다. 또한 화살표 함수는 호출 시 this 바인딩을 하지 않기 때문에 선언된 시점에서의 this를 참조하게 된다. 따라서 이 화살표 함수의 this는 객체 리터럴이 정의된 외부 스코프의 this를 가리키게 된다.
정리하면 인스턴스의 메서드로 호출 했을 때 메서드 내부에서 this는 반드시 인스턴스를 가리킨다. 하지만 객체 리터럴의 메서드 내에서 this를 호출할 경우, 일반 함수로 선언한 메서드는 해당 객체를 가리키지만 화살표 함수로 선언한 메서드는 해당 객체가 선언된 스코프를 가리킨다. 그 이유는 객체 리터럴은 스코프를 가지지 않기 때문이다.
일반 함수에서의 this
일반 함수로 호출한 함수 내에서의 this는 메서드로 호출한 함수 내에서의 this와 달리 항상 전역 객체를 가리킨다.
normalFunction();
위 함수의 정의 부분의 예시로 다음과 같다.
function normalFunction() {
console.log(this);
}
normalFunction(); // Object [global] { ..
하지만 만약 정의한 함수가 화살표 함수라면?
const arrowFunction = () => {
console.log(this);
};
arrowFunction(); // {}
화살표 함수 또한 선언된 환경이 전역 스코프이므로 전역 객체를 가리킨다.
일반 함수와 화살표 함수 내에서 전역 객체 형태가 다른 이유
그런데 여기서 일반 함수와 화살표 함수 내에서의 전역 객체 형태가 다르다는 것을 확인할 수 있다. 이는 Node.js의 모듈 시스템과 관련이 있다. 일반 함수 내에서 this가 global을 가리키는 이유는 global이 Node.js의 전역 객체이기 때문이다. 그리고 화살표 함수 내에서 this가 빈 객체
{}를 가리키는 이유는, Node.js가 기본적으로 CommonJS 모듈 시스템 방식을 따르기 때문이다. CommonJS 모듈시스템 방식에서 각 파일은 함수로 감싸져 실행되며, 각 빈 객체{}로 설정된다.
정리하면, 일반 함수를 호출하면 함수 내부의 this는 전역 객체를 가리킨다. 여기서 일반 함수로 선언한 함수에서 this는 전역 객체를(브라우저 런타임 환경에서는 window, Node.js 런타임 환경에서는 global) 가리키고, 화살표 함수로 선언한 함수에서 this는 빈 객체를(Node.js CommonJS 모듈 기준) 가리킨다.
this를 바꾸는 법
호출할 함수 내의 this를 바꾸려면 크게 두 가지 방법이 있다. 화살표 함수를 사용하거나 bind, apply, call같은 메서드를 사용하거나.
화살표 함수는 함수를 호출했을 때 this바인딩을 하지 않는다. 그래서 화살표 함수로 선언한 함수 내에서 this를 호출했다면, 이는 스코프를 타고 올라가 this를 찾게된다. 다시말해, 화살표 함수가 선언된 시점의 스코프에서 this를 찾는다.
bind, apply, call은 모두 유사한 메서드이다. bind, apply, call 모두 첫 번째 인자에 this를 바인딩하고 싶은 객체를 전달한다. bind는 this를 바인딩한 함수를 반환하는 메서드이고 apply와 call은 this를 바인딩하여 즉시 실행하는 메서드이다. 또한 apply는 두 번째 인자에 배열 형태로 전달하고, bind와 call은 두 번째 인자부터 하나씩 전달한다.
- bind
const obj = {
name: "박규성",
};
function hello(nickname, age) {
console.log(this.name, nickname, age);
}
const hello2 = hello.bind(obj, "피터", 20); // obj, 피터, 20
hello2();
- apply
const obj = {
name: "박규성",
};
function hello(nickname, age) {
console.log(this.name, nickname, age);
}
hello.apply(obj, ["피터", 20]); // obj, 피터, 20
- call
const obj = {
name: "박규성",
};
function hello(nickname, age) {
console.log(this.name, nickname, age);
}
hello.apply(obj, "피터", 20); // obj, 피터, 20
맺으며
JavaScript에서 this에 대해 살펴보았다.
이때 주목할 점은 함수 선언 방식에 따라서도 this의 바인딩이 달라진다는 것이다. 일반 함수는 호출 시점에 this가 결정되지만, 화살표 함수는 선언된 시점의 스코프에서 this를 찾는다. 특히 객체 리터럴 내에서 화살표 함수를 메서드로 사용할 때 예상치 못한 결과를 얻을 수 있으므로 주의해야 한다. 또한 bind, apply, call과 같은 메서드를 통해 의도적으로 this를 바인딩할 수도 있다. 특히 콜백에서 컨텍스트 유지할 때 자주 쓴다.
결국 호출·선언 방식을 같이 봐야 this가 보인다. 이거 알고 나니 콜백 디버깅이 훨씬 수월해졌다.