자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됩니다.
실행 컨텍스트에 관한 글은 아래를 참고하세요.
실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다고 할 수 있겠습니다. 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라지는 것입니다.
전역 공간에서 this는 전역 객체(브라우저 환경에서는 window, Node.js 환경에서는 global)를 가리킵니다. 개념상 전역 컨텍스트를 생성하는 주체가 바로 전역 객체이기 때문입니다.
함수를 실행하는 방법은 여러 가지가 있는데, 가장 일반적인 방법 두 가지로는 함수로서의 호출과 메서드로서의 호출 두 가지입니다.
둘을 어떻게 구분하냐고요? 함수 앞에 점(.)이 있으면 메서드로서 호출한 것입니다(물론 대괄호 표기법에 따른 경우에도 메서드로서 호출한 것입니다).
const obj = {
methodA: function () { console.log(this); },
inner: {
methodB: function () { console.log(this); }
}
};
obj.methodA(); // { methodA: f, inner: { ... } } ( === obj )
obj.inner.methodB(); // { methodB: f } ( === obj.inner )
this에는 호출한 주체에 대한 정보가 담깁니다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체입니다. 따라서 this는 점 앞에 명시된 객체가 되는 것이죠.
어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않습니다. this에는 호출한 주체에 대한 정보가 담긴다고 했습니다. 그런데 함수로서 호출한 경우 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없는 것입니다. this가 지정되지 않은 경우 this는 전역 객체를 바라보게 됩니다.
아래 코드를 보며 this가 무엇을 가리킬지 예상해 보세요.
(2)는 inner를 호출한 결과를, (3)은 obj2.inner를 호출한 결과입니다.
01 const obj1 = {
02 outer: function () {
03 console.log(this); // (1)
04
05 const inner = function () {
06 console.log(this); // (2) (3)
07 }
08 inner();
09
10 const obj2 = {
11 innerMethod: inner
12 };
13 obj2.inner();
14 }
15 };
16 obj1.outer();
정답은 (1) : obj1, (2) : 전역 객체(window), (3) : obj2입니다.
16번째 줄에서 outer 함수는 호출할 때 함수명인 outer 앞에 점(.)이 있었으므로 메서드로서 호출한 것입니다. 따라서 this에는 마지막 점 앞의 객체인 obj1이 바인딩됩니다.
8번째 줄에서 inner 함수는 호출할 때 함수명 앞에 점(.)이 없었습니다. 즉 함수로서 호출한 것이므로 this가 지정되지 않았고, 따라서 자동으로 전역객체(window)가 바인딩됩니다.
13번째 줄에서 inner 함수는 호출할 때 함수명 앞에 점(.)이 있었으므로 메서드로서 호출한 것입니다. 따라서 this에는 마지막 점 앞의 객체인 obj2가 바인딩됩니다.
한가지 고민이 생겼다고 가정해볼게요.
호출 주체가 없을 때는 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있다면 좋겠습니다.
변수를 검색하면 우선 가장 가까운 스코프의 LexicalEnvironment를 찾고 없으면 상위 스코프를 탐색하듯이, this 역시 현재 컨텍스트에 바인딩된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 말이죠. 그게 훨씬 자연스럽지 않나요?
const obj = {
outer: function () {
console.log(this); // { outer: f } ( === obj )
const self = this;
const inner = function () {
console.log(self); // { outer: f } ( === obj )
};
inner();
}
};
obj.outer();
우회라고 할 수도 없을 만큼 허무한 방법이지만 기대에는 충실히 부합하네요.
const obj = {
outer: function () {
console.log(this); // { outer: f } ( === obj )
const inner = () => {
console.log(this); // { outer: f } ( === obj )
};
inner();
}
};
obj.outer();
ES6에서는 this를 바인딩하지 않는 화살표 함수가 도입되었습니다.
화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어 this가 아예 존재하지 않습니다. 따라서 접근하고자 하면 스코프 체인상에서 가장 가까운 this에 접근하게 됩니다.
const A = function (name) {
this.name = name;
};
const b = new A("jaewon");
console.log(b); // { name: "jaewon" }
어떤 함수가 생성자 함수로서 호출된 경우 this는 곧 새로 만들 인스턴스 자신이 됩니다.
setTimeout(function () { console.log(this); }, 1000); // window
document.querySelector("something").addEventlistener("click", function(e) => {
console.log(this); // something 엘리먼트
})
setTimeout 함수는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않으므로 전역객체가 출력됩니다.
addEventlistener 함수는 메서드명의 점(.) 앞부분이 this가 되겠네요.
const func = function (a, b) {
console.log(this, a, b);
}
func(1, 2); // window, 1, 2
func.call({ x: 1 }, 1, 2); // { x: 1 }, 1, 2
call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령입니다.
첫 번째 인자를 this로 바인딩하고, 이후의 인자들은 호출할 함수의 매개변수로 합니다.
const func = function (a, b) {
console.log(this, a, b);
}
func(1, 2); // window, 1, 2
func.apply({ x: 1 }, [1, 2]); // { x: 1 }, 1, 2
call 메서드와 기능적으로 완전히 동일하며, 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 차이가 있겠네요.
const func = function (a, b) {
console.log(this, a, b);
}
func(1, 2); // window, 1, 2
const bindFunc = func.bind({ x: 1 });
bindFunc(1, 2) // { x: 1 }, 1, 2
call 메서드와 비슷하지만 즉시 호출하지는 않고 넘겨받은 this를 바탕으로 새로운 함수를 반환합니다.