this
는 통상 클래스로 생성한 인스턴스 객체를 의미한다.
그러나 다른 언어에서와 달리 자바스크립트에서 this
는 (클래스 내부가 아니어도) 어디서든지 사용할 수 있으며, this
를 출력하면 예상과 다르게 동작할 때가 많다.
this
는 함수가 호출될 때 결정된다.
지금부터 전역 공간, 함수, 메서드, 화살표 함수, 콜백 함수, 생성자 함수 내부에서
this
를 사용할 때 어떤 객체가 바인딩 되는지 알아보도록 하겠다.
전역 공간에서 this
는 전역 객체를 가리킨다.
여기서 전역 객체는 JS 런타임 환경에 따라 달라지는데,
브라우저 환경에서는 window
이고 Node.js 환경에서는 global
이다.
그리고 이 전역 객체는 프로퍼티를 가지는데
전역 공간에서 선언한 모든 변수는 전역 객체의 프로퍼티로 등록된다!
아래 예제를 살펴보자.
var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1
전역 변수 a
에 1을 할당하였더니 a
, window.a
, this.a
모두 1이 출력되었다.
위의 예제를 통해 전역 변수는 global
혹은 window
의 프로퍼티라는 것을 알 수 있다.
그렇다면 지역 변수는 어떨까?
자바스크립트의 모든 변수는 실행 컨텍스트의
LexicalEnvironment
객체의 프로퍼티로서 동작한다!
따라서 전역 공간에서 선언한 변수는 전역 컨텍스트의 LexicalEnvironment
객체의 프로퍼티이며,
함수 내부에서 변수를 선언하더라도 그 변수는 실행 컨텍스트의 LexicalEnvironment
객체의 프로퍼티가 된다.
🚨
하지만 전역 컨텍스트의 L.E
가 전역 객체(this
)를 그대로 참조할 뿐
지역 실행 컨텍스트의 L.E
와 this
는 별개이므로 혼동하지 않도록 주의하자!
어떤 함수를 호출할 때는 함수로서 호출하는 경우와 메서드로서 호출하는 경우 2가지가 있다.
둘은 무슨 차이일까? 아래 예제를 통해 설명하도록 하겠다.
var func = function (x) {
console.log(this, x);
};
func(1);
var obj = {
method: func
};
obj.method(2);
위의 코드에서 func
이라는 동일한 함수를 그냥 함수로서 호출하는 경우와
obj
객체의 프로퍼티로 등록 후 객체의 메서드로서 호출하는 경우를 확인할 수 있다.
func
을 그냥 함수로서 호출하면 this
는 전역 객체인 window
가 된다.
그러나 프로퍼티 접근 연산자(.)로 연결하여 객체의 메서드로서 호출하면 this
는 obj
가 된다.
실제로 자바스크립트 엔진은 점(.) 연산자의 유무로 메서드로서 호출했는지, 함수로서 호출했는지 판단한다.
🚨 어떤 함수를 함수로서 호출하면 실행 컨텍스트가 생성될 때
this
바인딩을 따로 하지 않는다.
이처럼 this
가 따로 지정되지 않으면 전역 객체를 바라보게 된다.
이러한 점 때문에 우리는 this
가 실제 어떤 객체를 바라보고 있는지 예측할 수 없다.
아래 예제를 통해 this
가 어떤 객체를 참조하고 있는지 살펴보자!
var obj1 = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
}
innerFunc(); // window
var obj2 = {
innerMethod: innerFunc
};
obj2.innerMethod(); // obj2
}
};
obj1.outer(); // obj1
위에서 함수 실행문은 총 3개이다.
가장 먼저 실행되는 함수는 obj1.outer();
으로 프로퍼티 접근 연산자(.)을 이용하여 메서드로서 호출하였다.
따라서 이 함수 안에서 this
는 obj1
이다.
두 번째로 실행되는 함수는 innerFunc();
으로 그저 함수로서 호출하였다.
this
가 바인딩되지 않았으므로 전역 객체를 바라보게 된다.
따라서 this
는 window
이다.
세 번째로 실행되는 함수는 obj2.innerMethod();
으로 프로퍼티 접근 연산자(.)을 이용하여 메서드로서 호출하였다.
따라서 이 함수 안에서 this
는 obj2
이다.
이처럼 this
은 함수를 어떻게 호출하느냐에 따라 바라보는 객체가 달라진다.
함수를 메서드로서 실행하면 실행 컨텍스트가 생성될 때 this
는 점(.) 앞의 객체를 바인딩하며
함수를 그냥 함수로서 실행하면 this
바인딩을 하지 않으므로 전역 객체를 바라보게 되는 것이다!
함수 내부에서 this
가 전역 객체를 바라보는 문제를 보완하고자
ES6 부터 화살표 함수가 도입되었다!
화살표 함수는 실행 컨텍스트를 생성할 때 this
바인딩 과정 자체가 빠지게 되어
상위 스코프의 this
를 그대로 활용하게 된다.
var obj = {
outer: function () {
console.log(this);
var innerFunc = () => {
console.log(this);
};
innerFunc();
}
};
obj.outer(); // obj
위의 예제에서 obj.outer();
함수를 메서드로서 호출하였다.
따라서 가장 먼저 obj
객체가 출력된다.
그 다음으로 화살표 함수인 innerFunc();
을 함수로서 호출하는데
상위 스코프의 this
을 그대로 활용하여 obj
객체가 출력된다.
함수 A의 제어권을 다른 함수 B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다.
이때, 함수 A는 함수 B의 내부 로직에 따라 실행되며, this
역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정된다!
아래 예제 코드를 살펴보자.
setTimeout(function () { console.log(this); }, 300);
document.body.querySelector('#a')
.addEventListener('click', function (e) {
console.log(this, e);
});
0.3초 뒤에 실행되는 콜백 함수는 window
가 출력되며
id
가 a
인 요소를 클릭했을 때 실행되는 콜백 함수는 해당 id = a
인 요소를 출력하게 된다.
이는 addEventListener
함수가 콜백 함수에서의 this
를 무엇으로 할지에 대해 미리 정의해두었기 때문이다!
생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다.
자바에서의 class
와 유사하다고 생각하면 된다.
결론부터 말하자면 생성자 함수 내부에서의 this
은 곧 새로 만들 인스턴스 자신을 의미한다.
아래 예제를 살펴보자!
var Cat = function (name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
};
var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi); // 초코 객체와 나비 객체 출력
위 생성자 함수 안에서의 this
는 앞으로 생성될 객체에 접근하기 위함이다.
따라서 this
을 통해 하나의 생성자 함수로 다양한 속성을 갖는 객체를 생성할 수 있다. (다형성)
명시적으로 this
을 바인딩할 수 있는 방법으로 세 가지 메서드가 존재한다.
call
, apply
, bind
을 차례대로 살펴보자!
call
은 메서드의 호출 주체인 함수를 즉시 실행하며
메서드의 첫 번째 인자를 this
로 바인딩하고 두 번째 인자를 호출할 함수의 매개변수로 한다.
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func(1, 2, 3);
func.call({ x: 1 }, 4, 5, 6);
위 예제에서 func
을 그냥 함수로서 호출했을 때 this
은 전역 객체가 된다.
그러나 call
메서드를 이용하여 호출했을 때 첫 번째 인자로 전달된 객체인 { x: 1 }
이 this
가 된다!
apply
메서드는 call
메서드와 기능적으로 완전히 동일하다.
그러나 유일한 차이는 call
메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면,
apply
메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다.
아래의 예제를 살펴보자.
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func.call({ x: 1 }, 4, 5, 6); // { x: 1 } 4 5 6
func.apply({ x: 1 }, [4, 5, 6]); // { x: 1 } 4 5 6
같은 함수를 call
메서드와 apply
메서드 두 방식으로 호출해보았다.
인자를 전달하는 방식만 다를 뿐 두 실행문 모두 같은 출력 결과를 가진다.
bind
메서드는 위의 두 메서드와 다르게 즉시 호출하지 않고
전달받은 this
와 인수들을 바탕으로 새로운 함수를 반환한다.
아래의 예제를 살펴보자.
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
var bindFunc = func.bind({ x: 1 }, 4, 5);
bindFunc(6, 7);
bind
메서드를 이용하여 새로운 함수 bindFunc
을 반환하였다.
이 bindFunc
을 호출하면 인자로 전달된 { x: 1 }
객체가 this
로서 출력되며,
인자로 전달된 4
와 5
도 순서대로 파라미터 a
와 b
에 전달된다.
즉, 최초 func
함수에 4, 5, 6, 7을 인자로 넘기고 { x: 1 }
객체가 this
로 바인딩된 것과 같다.
이처럼 call
, apply
, bind
메서드를 이용하면
this
를 예측할 수 있으므로 개발자들의 혼란을 막을 수 있다.
또한 화살표 함수를 이용하여 상위 스코프의 this
를 그대로 이용할 수 있다.