this 바인딩
- this binding은 실행 컨텍스트가 활성화 될 때 한다.(= 실행 컨텍스트가 생성되는 순간에 this를 바인딩한다.)
- 실행 컨텍스트는 컨텍스트에 해당하는 함수가 호출될 때 생성된다. 곧 this는 함수가 호출될 때에 비로소 결정되는 것이다. 그렇기에 정적으로 코드만 봤을 때 어떤 것의 this가 무엇이다 바로 예측할 수 있는 것이 아니라 이 함수를 어떤 식으로 호출했느냐에 따라서 this가 얼마든지 달라질 수 있다.(= this는 동적으로 바인딩 된다.)
호출하는 방식에 따른 this
* 아래의 다섯가지에 대한 차이점을 이해하면 this에 대해 완벽하게 정리할 수 있다.
- 전역공간에서 호출할 경우
- 함수를 함수로서 호출할 경우
- 함수를 메서드로서 호출할 경우
- 함수를 callback 함수로 넘겼을 때
- 함수를 생성자 함수로 호출할 경우
1. 전역공간에서 호출할 경우
- 전역공간은 무조건 처음부터 호출되기에, 여기서 this는 정해져 있다. 그런데 나머지 것들(2번 ~ 5번)에 대해서는 this가 다 다르다.
- 전역 공간에서의 this는 전역 객체(window - 브라우저 환경, global - nodejs 환경)를 가리킨다.
- 전역 컨텍스트를 실행하는 주체가 전역 객체(= 호스트 객체)이다. 런타임에 따라 전역 객체의 정보가 달라진다.(window 혹은 global)
- window, global 모두 ECMAScript에서 정의한 '전역 객체'가 아닌, ECMAScript에서 정의된 전역 객체의 내용(특성)을 바탕으로 런타임(브라우저, 노드)에서 제공하는 구현체이다.
2. 함수를 함수로서 호출할 경우
- 함수 내부에서의 this또한 전역 객체(window/global)를 가리킨다.
function a() {
console.log(this);
}
a();
- this가 출력될 때, 전역객체를 출력하는 것이 이상하게 여겨질 수 있는데 위와 같은 코드에서 a라는 함수를 호출하는 순간에 이것을 실행해주는 주체가 누구인지 생각해봤을 때 '전역 객체'라고 생각하는 것이 좋다.
function b() {
console.log(this); // (1)
function c() {
console.log(this); // (2)
}
}
b();
- 위와 같은 코드에서 (1) 결과가 전역객체가 나오는 것은 어떻게든 이해해줄 수 있는데, (2)에서도 전역객체가 나오는 것은 납득이 안될 수 있다. 이것을 JS의 실수라고 보는 견해도 있다. 이러한 견해가 수렴되어 ES6에서는 this 바인딩을 하지 않는 arrow function이 나왔다. arrow function은 바로 위 컨텍스트에 있는 this를 그대로 가져다 사용한다. 따라서 ES6에서는 이 함수를 통해 이 문제점은 해소가 되는데, ES5 환경에서는 함수로써 호출했을 때 this는 무조건, 언제나 전역 객체를 가리킨다.(*암기*)
3. 함수를 메서드로서 호출할 경우
- 메서드를 호출한 주체(메서드명의 '점' 바로 앞이 this)
# 1번
var a = {
b: function() {
console.log(this);
}
}
// b라는 메서드를 a.으로 호출했다. 곧 점(.) 바로 앞이 this가 된다.(a가 this)
// b함수를 a 객체의 "메서드"로서 호출
a.b();
# 2번
var d = {
e: function () {
function f() {
console.log(this);
}
f();
}
}
d.e(); // 전역객체가 출력. f라는 함수가 함수로써 호출되었기 때문.
# 3번
var a = {
b: {
c: function() {
console.log(this);
}
}
}
a.b.c(); // this는 a.b
- 메서드는 아래와 같은 방법으로도 호출이 가능하다.
// this는 obj
obj.func();
obj['func']();
// this는 person.info
person.info.getName();
person.info['getName']();
person['info'].getName();
person['info']['getName']();
메서드 내부 함수에서 전역객체를 출력하지 않게끔 하는법
var a = 10;
var obj = {
a: 20,
b: function() {
console.log(this.a); // (1)
function c() {
console.log(this.a); // (2)
}
c();
}
}
obj.b();
- (1)에서는 b가 메서드로서 호출되었기에 obj 내에 있는 a를 찾아 20을 출력한다.
- (2)에서는 c가 함수로서 호출되었기에 전역객체에 있는 a를 찾아 10을 출력한다.
- call, apply와 같은 명시적인 this 바인딩 명령을 쓰지 않고는 this 자체를 직접 다른 값으로 덮어씌울 수 없다.
- 스코프 체인을 활용하여 위와 같은 함수를 사용하지 않고 this를 다른 값으로 우회할 수 있다. 스코프 내 외부 환경 참조를 통하여 this를 가져오는 것이다. 아래의 코드 내용과 같다.
var a = 10;
var obj = {
a: 20,
b: function() {
var self = this; // obj.b()와 같이 호출된다고 했을 때, 이때의 this는 obj인데, 이 값을 self 담는다. 개발자들의 취향에 따라 self라는 변수명 대신 _this, that과 같은 것들이 종종 쓰인다.
console.log(this.a); // (1)
function c() { // 내부 함수는 자신의 LexicalEnvironment에서 self를 찾고, 해당 내용이 없기에 outerEnvironmentReference를 타고 외부(b)에 있는 함수의 LexicalEnvironment에서 self를 찾는다.
console.log(self.a); // (2)
}
c();
}
}
obj.b();
// 출력결과: (1) - 20 / (2) - 20
- 아래와 같은 방법으로 a를 표현하면 a가 10이 아닌 20을 출력할 수 있게 된다.
// ES6 arrow function
var a = 10;
var obj = {
a: 20,
b: function() {
console.log(this.a);
const c = () => {
console.log(this.a);
}
c();
}
}
obj.b();
// ES5 call & apply
var a = 10;
var obj = {
a: 20,
b: function() {
console.log(this.a);
function c() {
console.log(this.a);
}
c.call(this);
}
}
obj.b();
4. 함수를 callback 함수로 넘겼을 때
- 기본적으로는 함수 내부에서와 동일.
function a(x, y, z) {
console.log(this, x, y, z);
}
var b = {
bb: 'bbb'
}
a.call(b, 1, 2, 3); // (1)
a.apply(b, [1, 2, 3]); // (2)
var c = a.bind(b);
c(1, 2, 3) // (3)
var d = a.bind(b, 1, 2);
d(3); // (4)
- 위 1,2,3,4 모두 {bb: 'bbb'} 1 2 3 이 출력된다.
- 3가지 함수(call, apply, bind)의 API 스펙을 보면 아래와 같다.
명시적인 this binding
[ ] 안에 있는 것은 생략 가능한 것.
func.call(thisArg[, arg1[, arg2[, ... ]]])
func.apply(thisArg, [argsArray])
func.bind(thisArg[, arg1[, arg2[, ... ]]])
- func를 호출할 때, this를 첫번째 인자로 인식하게 해달라고 개발자가 직접 명시한다.
var callback = function() {
console.dir(this); // callback 함수 내부에서의 this는 콜백 함수 자체가 결정할 수 있는 것이 아니라, callback 함수를 전달받는 곳 즉, b 속성 내 cb를 전달받는 곳에서 어떻게 처리하냐에 따라 this가 달라진다.
};
var obj = {
a: 1,
b: function(cb) {
cb(); // window 출력
cb.call(this); // obj 출력
}
};
obj.b(callback);
- 위 주석에 대한 내용을 더 구체적으로 이해하기 위해 아래의 코드를 살펴보자.
# 1번
var callback = function() {
console.dir(this);
}
var obj = {
a: 1
}
// setTimeout(callback, 100); -> window 출력
// setTimeout(callback.bind(obj), 100); -> obj 출력
# 2번
document.body.innerHTML += '<div id="a">클릭</div>';
document.getElementById('a').addEventListener(
'click',
function() {
console.dir(this); // 이때는 window가 아닌 element 즉, div#a 가 나온다. 왜냐하면 addEventListener 내부에서 콜백함수를 처리할 때 전역객체가 아닌 이벤트 타겟이 된 엘리먼트를 출력하게끔 되어 있기 때문.
}
)
정리
- 기본적으로는 함수의 this와 같다.
- 제어권을 가진 함수가 콜백의 this를 지정해둔 경우도 있다.
- 이 경우에도 개발자가 this를 binding해서 콜백을 넘기면 그에 따른다.
5. 함수를 생성자 함수 호출 시
- new 연산자를 사용한 경우 this의 출력.
- new 연산자를 사용했다는 말은 생성자 함수의 내용을 바탕으로 인스턴스 객체를 만드는 명령. 이때는 새로 만들 인스턴스 객체 그 자체가 this가 된다.
function Person(n, a) {
this.name = n;
this.age = a;
}
var roy = Person('hnoo', 30);
console.log(window.name, window.age); // hnoo 30 출력(함수로서 Person을 출력했기에)
function Person(n, a) {
this.name = n;
this.age = a;
}
var roy = new Person('hnoo', 30);
console.log(window.name, window.age); // '' '' 출력
console.log(roy.name, roy.age); // Person{name: 'hnoo', age: 30} 출력
'javascript' 카테고리의 다른 글
[CoreJavascript] 5과 클로저(closure) (1) | 2025.02.15 |
---|---|
[CoreJavascript] 4과 콜백함수(callback function) (0) | 2025.02.13 |
[CoreJavascript] 2과 실행컨텍스트 (0) | 2025.01.28 |
[CoreJavascript] 1과 데이터 타입 (0) | 2025.01.24 |
이벤트 위임(Event Deligation) (0) | 2022.02.20 |