this에 관하여
함수가 호출될 때 동적으로 바인딩된다.
호출 방식에 따라 바인딩이 달라진다.
호출되는 문맥 | 브라우저 | node.js |
전역공간에서 호출 시 | window | global |
함수 호출 시 | window | global |
메소드 호출 시 | 메소드 호출 주체(메소드명 앞) | |
callback 호출 시 | 기본적으로는 함수 내부에서와 동일 | |
생성자함수 호출 시 | 인스턴스 |
함수 호출시
그런데 이상하다. 함수 호출 시에도 전역공간에서 호출한 것과 같은 효과가 나타난다.
이 경우에는 함수의 호출이 전역공간에서 일어난 것이므로 자연스럽다고 볼 수 있다.
function a() {
console.log(this);
}
a();
함수 내에 함수가 호출된 경우에는 어떨까?
function b() {
function c() {
console.log(this);
}
c();
}
b();
이 경우 function c도 전역공간에서 호출한 것과 같은 동작이 일어난다.
이상하다. 심오한 의미가 있는걸까?
그럼 상위 문맥을 바인딩하기 위해서는 어떻게 해야할까?
이를 위해 등장한 것이 ES6의 arrow function이다.
b = () => {
c = () => {
console.log(this);
}
c();
}
b();
다음과 같이 변환해주면 문제 없다.
+ 하지만 arrow function의 경우, call등을 이용한 this 바인딩 임의 조작은 불가능하다.
메소드 호출 시
var a = {
b: function() {
function f() {
console.log(this);
}
f(); // 전역
console.log(this);
}
}
a.b();//메소드 호출객체
이 경우 b()를 호출한 a가 바인딩된다.
f()는 아무튼 함수형으로 호출되었기 때문에 전역객체가 바인딩된다
내부함수에서 우회하여 바인딩하기
그럼에도 불구하고 ES5환경에서 상위 문맥을 바인딩하는 우회법이 존재했다.
지금이야 레거시 코드겠지만, 나중에 코드를 읽을 때 어려움이 없도록 기록해두겠다.
var a = 10;
var obj = {
a: 20,
b: function() {
console.log(this.a);//obj
function c() {
console.log(this.a);//window
}
c();
}
}
obj.b();
위의 코드에서 obj.b()는 메소드로 선언되어있으므로 obj를 상위 문맥으로 바인딩한다.
그리고 c()는 함수선언으로 되어있으므로 전역객체를 바인딩한다.
var a = 10;
var obj = {
a: 20,
b: function() {
var self = this;
console.log(this.a);//obj
function c() {
console.log(self.a);
}
c();//obj
}
}
obj.b();
하지만 이 코드의 경우 c()는 obj를 바인딩한다.
코드가 실행되고 바인딩할 범위를 전역->외부->내부 함수 순으로 탐색하게 된다.
객체 obj내의 변수 self는 메소드 b를 바인딩하며, self를 참조하는 함수 역시 self가 가리키는 객체를 바인딩하게 되는 것이다.
++
위의 코드는 다음과 같이 훨씬 간단하게 작성할 수도 있다.
var a = 10;
var obj = {
a: 20,
b: function() {
var self = this;
console.log(this.a);//obj
{
console.log(this.a);
}
c();//obj
}
}
obj.b();
변수의 스코프를 구분하고 싶을 때는 중괄호만을 이용해서 구분할 수 있다.
콜백함수 호출 시
이 부분은 좀 이해하기 어려울 수 있다. 하지만 이 부분이 바인딩의 필요성을 가장 잘 나타내는 부분인 것 같다.
콜백함수를 호출할 때 어떠한 객체를 바인딩하도록 명시적으로 지정할 수 있는 것이다.
위의 코드들은 전부 어떠한 객체나 함수 내에 메소드나 함수를 선언하여 바로 상위 문맥을 바인딩하도록 했다.
하지만 명시적인 this바인딩을 이용하면 독립적으로 선언되어있는 함수나 객체 간에도 바인딩이 가능하다.
call, apply, bind를 이용하여 명시적인 바인딩이 가능하다.
세 메소드 모두 첫 인자로 this가 들어가야한다.
function a(x, y, z) {
console.log(this, x, y, z);
}
var b = {
c: 'eee'
};
a.call(b,1,2,3);// Object {c: "eee"} 1 2 3
a.apply(b, [1,2,3]);// Object {c: "eee"} 1 2 3
var c = a.bind(b);
c(1,2,3);// Object {c: "eee"} 1 2 3
var d = a.bind(b,1,2);
d(3);// Object {c: "eee"} 1 2 3
a.call(b,1,2,3) : 함수 a의 상위 문맥이 b로 바인딩된다.
코드로 보면 다음과 같이 변하는 것이다.
var b = {
c: 'eee'
function a(x, y, z) {
console.log(this, x, y, z);
}
};
대략적으로 이렇게 생겨먹은 코드가 실행이 되는 것이다.
apply, bind도 비슷하게 동작한다고 보면 된다.
call, apply, bind 메소드를 사용하지 않고 콜백함수로만 바인딩을 할 경우에는 어떨까?
var callback = function() {
console.dir(this);
};
var obj = {
a: 1,
b: function (cb) {
cb(); // window
cb.call(this); // Object
}
};
obj.b(callback);
cb(): 아무튼 cb() 함수로 선언했기 때문에안타깝게도 window 전역객체를 바인딩한다.
'기본적으로는 함수 내부에서와 동일'이라고 하지 않았나.
cb.call을 이용해야만 상위 문맥으로 바인딩이 가능하다.
콜백함수를 기본 인자로 받는 setTimeout은 어떻게 동작할까?
var callback = function() {
console.dir(this);
};
var obj = {
a: 1
};
setTimeout(callback, 100);//window
setTimeout(callback.bind(obj), 100);//Object
setTimeout은 기본적으로 this 바인딩처리를 해주지 않는다.
this바인딩을 해주고 싶다면 bind를 이용해야한다.
addEventListener는 어떻게 동작할까?
document.body.innerHTML += '<div id="a">클릭하세요</div>';
document.getElementById('a').addEventListener('click', function() {
console.dir(this); //div
});
addEventListener는 별도의 바인딩을 해주지 않더라도 이벤트의 타켓이 된 객체를 바인딩한다.
만약 인위적으로 바인딩 대상을 변경하고 싶다면 bind로 처리해주자.
document.body.innerHTML += '<div id="a">클릭하세요</div>';
var obj = { a: 1 };//바인딩하고 싶은 객체
document.getElementById('a').addEventListener('click', function() {
console.dir(this); //Object
}.bind(obj));
콜백함수의 바인딩을 정리하자면
- 기본적으로는 함수의 this와 같다
- 제어권을 가진 함수가 callback의 this를 명시한 경우 그에 따른다.
- 개발자가 this를 바인딩한 채로 callback을 넘기면 그에 따른다.
생성자 함수 호출 시
이게 아마 자바나 C++를 이용해본 사람들에게 가장 익숙한 패턴일 것이다.
function Person(n, a) {
this.name = n;
this.age = a;
}
var gomugom = new Person('고무곰', 30);
console.log(gomugom);
gomugom이라는 객체가 생성되며 Person의 this.는 gomugom에 바인딩된다.
출처: Javascript 핵심 개념 알아보기 - JS Flow, 정재남 강사님, 코어자바스크립트