상황에 따라 달라지는 this
자바스크립트의 this
자바스크립트의 this는 다른 언어와는 다릅니다.
Java의 경우를 봅시다.
public Class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
이 코드에서 this는 인스턴스 자신을 가리킵니다.
따라서 this.name
는 인스턴스의 멤버 변수 name을 가리킵니다.
그러나 자바스크립트는 함수가 호출되는 방식에 따라 this가 가리키는 대상이 달라집니다. 자바스크립트의 this는 기본적으로 실행 컨텍스트가 생성될 때 결정됩니다.
즉, 함수가 호출될 때 this가 가리키는 대상이 결정됩니다.
다양한 케이스에서의 this를 살펴봅시다.
1. 전역 공간에서의 this
전역 공간에서의 this는 전역 객체를 가리킵니다. 브라우저 환경에서는 window 객체가 전역 객체이며, Node.js 환경에서는 global 객체가 전역 객체입니다.
console.log(this); // { alert: f, console: f, ... }
console.log(window); // { alert: f, console: f, ... }
console.log(this === window); // true
자바스크립트는 전역변수를 선언하면 이를 전역 객체에 프로퍼티로 추가합니다.
사실, 자바스크립트의 모든 변수는 어떠한 객체의 프로퍼티로 존재합니다.
var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1
var
키워드를 이용하여 전역 변수를 선언하더라도 특정 객체의 프로퍼티로 추가됩니다.
여기서 특정 객체는 실행 컨텍스트의 Lexical Environment
가 됩니다. 실행 컨텍스트는 변수를 수집해서 Lexical Environment
의 프로퍼티로 저장합니다.
이후 어떤 변수를 참조하면, Lexical Environment
에서 해당 변수를 찾아 반환합니다.
실제로는 전역 변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로
할당한다는 것을 알 수 있습니다.
그렇다면 a를 직접 참조할 때도 1이
나오는 이유는 뭘까요?
대부분의 경우에는 전역 공간에 var
로 변수를 선언하는 대신 window
의 프로퍼티에 직접 할당하더라도 동일한 결과를 얻을 수 있습니다.
var a = 1;
window.b = 2;
console.log(a, window.a, this.a); // 1 1 1
console.log(b, window.b, this.b); // 2 2 2
window.a = 3;
b = 4;
console.log(a, window.a, this.a); // 3 3 3
console.log(b, window.b, this.b); // 4 4 4
그러나 '삭제'연산에 대해서는 다르게 동작합니다.
var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1
var b = 2;
delete b; // false
console.log(b, window.b, this.b); // 2 2 2
window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // ReferenceError: c is not defined
window.d = 4;
delete d; // true
console.log(d, window.d, this.d); // ReferenceError: d is not defined
처음부터 전역객체의 프로퍼티로 할당한 경우 삭제가 되지만, 전역변수로 선언한 경우 삭제가 되지 않습니다.
전역 변수를 선언하면 자바스크립트 엔진이 자동으로 전역 객체의 프로퍼티로 할당하는데, 이 때 configurable
속성이 false
로 설정됩니다.
2. 메서드로서 호출할 때 내부에서의 this
함수 vs 메서드
함수와 메서드의 차이는 호출 방식
에 있습니다. 이 둘을 구분하는 차이는 독립성입니다.
함수는 독립적으로 호출되지만, 메서드는 객체에 속해있어 객체의 프로퍼티로 호출됩니다.
자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했습니다.
var func = function (x) {
console.log(this, x);
};
func(1); // Window { ... }, 1
var obj = {
method: func,
};
obj.method(2); // { method: f }, 2
어떤 함수를 객체의 프로퍼티에 할당한다고 해서 메서드가 되는 것이 아닙니다.
객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작합니다.
위의 예제를 보면, 동일한 익명함수를 호출하였지만, 두 경우에서 this가 가리키는 대상이 다릅니다.
메서드 내부에서의 this
this에는 호출한 주체에대한 정보가 담깁니다.
메서드 내부에서 this는 메서드를 호출한 객체를 가리킵니다.
var obj = {
methodA: function () {
console.log(this);
},
inner: {
methodB: function () {
console.log(this);
},
},
};
obj.methodA(); // { methodA: f, inner: { methodB: f } }
obj['methodA'](); // { methodA: f, inner: { methodB: f } }
obj.inner.methodB(); // { methodB: f }
obj.inner['methodB'](); // { methodB: f }
obj['inner'].methodB(); // { methodB: f }
obj['inner']['methodB'](); // { methodB: f }
3. 함수로서 호출할 때 내부에서의 this
함수 내부에서의 this
함수를 함수로서 호출할 경우 this가 지정되지 않습니다.
this에는 함수를 호출한 주제가 담긴다고 했습니다. 따라서 함수로서 호출하는 경우에는 호출 주체가 없습니다.
this가 지정되지 않은 경우에 this는 전역 객체를 가리킵니다. 따라서 함수를 함수로서 호출할 경우 this는 전역 객체를 가리킵니다.
더글라스 크락포드(Douglas Crockford)는 이를 명백한 설계상의 오류라고 지적하였습니다.
메서드 내부함수에서의 this
var obj1 = {
outer: function () {
console.log(this); // (1)
var innerFunc = function () {
console.log(this);
};
innerFunc(); // (2)
var obj2 = {
innerMethod: innerFunc,
};
obj2.innerMethod(); // (3)
},
};
obj1.outer();
위 코드에서 (1)의 this는 obj1
을 가리킵니다.
(2)의 this는 전역 객체를 가리킵니다.
(3)의 this는 obj2
를 가리킵니다.
메서드 내부함수에서의 this가 전역 객체를 가리키는 이유는 무엇일까요?
메서드 내부함수에서의 this를 우회하는 방법
이렇게 하면 함수 호출 주체에 따른 this의 구분은 명확히 할 수 있습니다.
하지만 직관적으로 함수 호출 주체가 없을 때에는 호출 함수 상위의 this를 상속받았으면 좋을 것 같습니다.
변수를 검색하면 가장 가까운 스코프의 Lexical Environment
에서부터 검색하는 스코프 체인처럼 this 역시 현재 컨텍스트에 바인딩된 대상이 없으면 상위 컨텍스트의 this를 참조하는 방법이 있으면 좋을 것 같습니다.
변수를 활용하여 this를 우회할 수 있습니다.
var obj = {
outer: function () {
console.log(this); // (1) { outer: f }
var innerFunc = function () {
console.log(that); // (2) Window { ... }
};
innerFunc();
var self = this;
var innerFunc2 = function () {
console.log(self); // (3) { outer: f }
};
innerFunc2();
},
};
obj.outer();
self
나 that
와 같은 변수를 선언하여 this를 우회할 수 있습니다.
변수가 컨텍스트에 바인딩되는 것을 이용한 방법입니다.
() => { }
ES6에서는 화살표 함수를 도입하였습니다.
화살표 함수는 😇 this 바인딩 과정 자체가 빠지게 되어 😇 상위 스코프의 this를 그대로 활용할 수 있습니다.
var obj = {
outer: function () {
console.log(this); // (1) { outer: f }
var innerFunc = () => {
console.log(this); // (2) { outer: f }
};
innerFunc();
},
};
obj.outer();
그 밖에도 bind
, call
, apply
를 이용하여 this를 지정할 수 있습니다.
이에 대한 내용은 다음 챕터에서 다루겠습니다.
4. 콜백 함수 내부에서의 this
콜백 함수의 구체적인 동작 원리와 정의에 대한 내용은 다음 장에서 다루겠습니다.
여기서는 this 바인딩에 대한 내용만 다루겠습니다.
함수 A의 제어권을 다른 함수 B에게 넘겨주는 것을 콜백 함수라고 합니다.
이때, 함수 A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부 로직에서
정한 규칙에 따라 값이 결정됩니다.
대표적인 콜백 함수의 예시를 확인해봅시다.
setTimeout(function () { console.log(this) }, 300); // (1)
[1, 2, 3, 4, 5].forEach(function (x) { // (2)
console.log(this, x);
});
document.body.innerHTML = '<button id="a">클릭</button>';
document.body.querySelector("#a")
.addEventListener('click', function (e) { // (3)
console.log(this, e);
});
(1)의 경우, setTimeout
함수의 콜백 함수 내부에서 this는 전역 객체를 가리킵니다.
(2)의 경우, forEach
메서드의 콜백 함수 내부에서 this는 전역 객체를 가리킵니다.
(3)의 경우, addEventListener
메서드의 콜백 함수 내부에서 this는 자신, 즉 addEventListener
메서드를 가리킵니다.
이처럼 콜백 함수 내부에서의 this는 호출한 함수에 따라 달라집니다.
5. 생성자 함수 내부에서의 this
생성자 함수는 객체를 생성하는 함수입니다. 객체와 클래스는 7장에서 다루도록 하겠습니다.
자바스크립트에서는 함수에 생성자로서의 역할을 부여했습니다. new
키워드를 사용하여 함수를 생성자 함수로 호출하면 해당 함수가 생성자로서 동작하게 됩니다.
그리고 어떤 함수가 생성자 함수로 호출되면, this는 생성자 함수가 생성할 인스턴스를 가리킵니다.
자바스크립트에서 인스턴스가 만들어지는 과정은 다음과 같습니다.
- 생성자 함수 호출
- 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티를 가진 빈 객체 생성
- 생성자 함수 내부의 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);
/** 결과
* Cat { bark: '야옹', name: '초코', age: 7 }
* Cat { bark: '야옹', name: '나비', age: 5 }
*/
Cat
이라는 변수에 익명 함수를 할당했습니다. 이 함수는 name
과 age
를 인자로 받아 this
에 bark
, name
, age
를 할당합니다.
new
키워드를 사용하여 Cat
함수를 호출하면 this
는 빈 객체를 가리키게 됩니다.
이후 this
에 bark
, name
, age
를 할당하고, 이를 반환합니다.
이렇게 생성된 인스턴스는 choco
, nabi
에 할당되어 출력됩니다.
즉 6번 줄에서 실행한 생성자 내부에서의 this는 choco
를 가리키고, 7번 줄에서 실행한 생성자 내부에서의 this는 nabi
를 가리킵니다.