시작하며
드디어 this 이해에 대한 종착점에 다다렀다. 이번 포스트에서는 ES6 스펙을 살펴보면서
이런 골칫덩어리 this를 어떤식으로 해결하려 했는지를 파악해본다.
=> 화살표 함수에서 this의 의미는 ??
=> (화살표) 함수는 기존의 function를 비교했을때 단순히 표기법만 바뀐 것이 아니다.
화살표 함수와 기존의 function()은 여러가지 다른점이 있지만 우리는 this에 대해서만 집중하도록 하자.
2가지만 확실히 알아두면 된다.
첫째, 기존에 function은 thisBinding이 이뤄지는 시점이 호출되는 시점이라고 이야기를 했었다.
화살표함수는 호출되는 시점에 thisBinding 대상을 문맥에서 찾는다.
화살표함수 자신을 감싸고 있는 문맥(스코프)의 this를 바인딩한다는 것이다.
완벽하게 맞지는 않지만 쉽게 기억할 수 있는 방법으로 설명을 하자면,
기존 처럼 호출 시점에 호출한 대상을 바라보는것이 아니라,
자신이 어디에 존재하는지, 어디서 정의되었는지를 파악한다고 보면 편하다. 자바같은 객체지향 언어에서 흔히 볼 수 있는 환경이다.
둘째, 화살표함수는 이름이 없는 익명함수이다. 익명함수는 이름이 없기때문에 변수에 담아두는 행동은 잘 하지 않는다. 그만한 이유가 있는데 바로 this 바인딩과 관련이 있기때문이다.
(즉 매 콜백함수로 많이쓰임) .
바로 이전 포스트 마지막부분에 추가를 했었는데, 익명함수는 이름이 없는 내부함수와 같다. 이해를 돕기위해 짤막한 코드하나를 준비했다.
object = {
foo : function(){
let bar = function(callback){
callback();
}
bar(()=>{});
}
}
객체에 foo라는 함수가 하나 정의되어 있고, foo 함수는 bar 함수를 내부함수로 가지고 있다.
bar 함수는 콜백함수를 받아서 실행시킨다. foo 함수는 bar 함수를 정의한후 바로 아래쪽에서 bar 함수를 실행시키는데, 매개변수로 화살표 함수를 넘겨주고 있다.
사실상 bar 함수와 매개변수로 들어간 화살표함수는 foo의 동일한 내부함수이다.
풀어쓰면
object = {
foo : function(){
let bar = function(callback){
callback();
}
let arrow = ()=>{console.log(this)};
bar(arrow);
}
}
2개의 코드는 같은 코드이다.
화살표 함수가 익명함수기는 하지만 변수에 담을 수는 있다. 저런식으로 쓰는 것은 권장하지 않는다. this 바인딩 시점이 헷갈려 파국을 가지고 올 것이다. 화살표함수는 변수에 담겨지거나 어떤 객체의 메소드로는 적합하지 않다.
자!
예제를 하나 준비했다. 다음 예제를 보고 console에 전역객체가 아닌 testObject가 찍히는 경우는 어떤때인가?
만약 하나도 틀리지 않고 예측할 수 있다면
축하한다. 당신은 this를 헷갈리지 않고 쓸 수 있을 것이다!
한번 더 정리를 해보고, 예측을 한번 해보기 바란다.
- 화살표함수는 this 바인딩을 자신을 감싸고 있는 스코프에서 실행한다. 호출시점에서 누가 자신을 호출했느냐가 중요한 것이 아니라, 자신이 어디에 위치했는지 파악한다.
- 화살표함수는 기본적으로 익명함수이다. 익명함수는 이름이 없는 내부함수와 같다.
let testObject = {
bar1: function () {
console.log(this);
},
bar2: () => { console.log(this) },
foo1: function (callback) {
callback();
},
foo2: function () {
setTimeout(function () {
console.log(this);
}, 1000);
},
foo3: function () {
setTimeout(() => {
console.log(this)
}, 1000);
},
foo4: function () {
this.foo1(() => {
console.log(this);
})
}
}
let bar3 = function () {
console.log(this);
}
let bar4 = () => console.log(this);
testObject.foo1(testObject.bar1);
testObject.foo1(testObject.bar2);
testObject.foo1(bar3);
testObject.foo1(bar4);
testObject.foo2();
testObject.foo3();
testObject.foo4();
가각 4개의 foo 함수와 bar 함수를 정의했다. foo 함수는 실행시키는 함수, bar 함수는 매개변수로 넘겨주는 함수이다.
정답 :
testObject.foo1(testObject.bar1); // window
testObject.foo1(testObject.bar2); // window
testObject.foo1(bar3); // window
testObject.foo1(bar4); // window
testObject.foo2(); // window
testObject.foo3(); // testObject
testObject.foo4(); // testObject
2개의 경우를 제외하고는 전부 window 객체와 바인딩이 된다.
풀이해보자!
먼저 1번과 2번을 보면,
testObject 의 foo1 함수를 호출했다. 즉 foo1 함수는 this로 testObject를 바인딩하였다.
그런데 왜 window가 바인딩 되었을까?
foo1: function (callback) {
callback();
}
그렇다. callback 함수가 호출되는 시점에서 왼쪽에 아무것도 명시되어 있지 않기때문에
전역 객체가 바인딩 되어버리는 것이다.
어? testObject.bar1,2 같은 형식으로 함수를 불러왔으니까
testObject.bar1 or testObject.bar2 이 호출된것 아냐? 라고 의구심을 가질 수 있다.
착각하면 안되는 것이 매개변수로 넘겨준 testObject.bar1 함수나 bar2 함수는 함수 그 자체만을 참조하고 있는 것이다.
testObject.bar 형식이라고 해서 testObject.bar 이런식으로 전체가 넘어간 것이 아닌, bar가 정의하고 있는 function 값만 넘어갔다고 보면 된다.
bar1 함수는 기존의 function 이니까 알고 있는대로 호출되는 시점에 바인딩될 객체가 명시되지 않았기때문에 이해가 가는데,
bar2 함수는 화살표함수다. 화살표 함수는 정의되는 시점에서 this 가 바인딩 된다고 이야기 했었다.
그럼 bar2 함수는 this를 testObject로 바인딩했어야 하는것 아닐까?
여기서 한가지 예외규칙이 있다. 화살표 함수가 객체의 prototype에 연결이 되면, this가 그 인스턴스에 바인딩 되지 않는다.
즉 ,object.bar = ()=>{} 와 같이 정의를 해도 this가 객체에 바인딩 되지않는다. 이는 화살표 함수가 이렇게 설계되었기때문에 익혀두어야 한다.
화살표 함수가 정의될때 바인딩 된다는 것은 어떤 함수스코프 안에 존재 할 때이다.
위 내용을 이해했다면 3번과 4번은 볼것도 없이 전역객체에 연결됨을 알수 있다.
동일하게 foo1 함수를 호출하고 있으며, bar3 , 4 함수는 객체에 연결되어 있지 않다.
bar3 함수는 기존의 방식대로 호출되는 시점에서 객체가 명시되지 않았으므로 전역객체에 연결된 것이고 bar 4 함수는 정의되어질때 특정 함수스코프 안에 있지 않기때문에 전역객체에 된다.
foo2: function () {
setTimeout(function () {
console.log(this);
}, 1000);
}
5번 문제를 보면 foo2 함수로 비동기 함수인 seTimeout 함수를 정의해두었다.
호출되는 시점은 1초뒤에 콜백함수로써 실행이 되어지는데, 내부에서 명시된 객체 없이 호출되어지므로 this는 전역객체에 바인딩 된다.
6번 문제는 5번문제와 달리 화살표 함수로 정의되어있다.
foo3: function () {
setTimeout(() => {
console.log(this)
}, 1000);
}
testObject.foo3(); // testObject
화살표 함수는 호출시점이 아니라, 정의된 시점에서 this가 바인딩 된다고 했다.
이 문제의 this는 어떻게 바인딩 되어있는지 곰곰히 생각해보자.
자바스크립트에서는 함수도 객체이므로 this는 foo3에 묶이는 것이 아닐까?
문제를 잘 보자.
this를 바인딩하려고 foo3을 바라보니 이 녀석은 function 키워드로 정의된 testObject 객체의 메소드이다.
일단 객체가 아니다. 메소드의 this는 testObject 이므로, 정의 되는 시점에서 바인딩 되는 this는 testObject 임을 알아낼수 있다.
마지막으로 7번 문제를 보자.
foo4: function () {
this.foo1(() => {
console.log(this.arrow);
})
},
foo4 함수는 메소드이며 this로부터 불러낸 foo1 함수에 화살표 함수를 넣어주고 있다.
저 화살표 함수의 this는 누구일까. 6번문제와 전혀 다를게 없다. 동일하게 내부함수이기때문에 정의된 시점에서 문맥을 따라 올라가면 testObject가 나오게 된다.
그렇다면 this.foo1의 this는 누구인가도 궁금할 것이다. 근데 이건 확인할 것도 없는것이
foo4는 testObject의 메소드이기때문에 이 함수를 쓰려면 testObject에 . 연산자을 이용해 접근할 수 밖에 없다. 즉 this.foo1 의 this는 명확하게 testObject임을 알 수 있다.
만약 함수가 그 자체로 객체가 되었다면 this가 바인딩 될 것인가.
마치며
머리가 지끈지끈 할 것 같다. 나름 쉽게 설명하려고 했었는데 나 역시 부족한 점이 많아, 어렵게만 적은 것 같다. 혹여나 잘못된 지식을 적은건 아닌지 걱정되기도 한다.
혼선이 걱정되는 부분도 있고, 헷갈리는 분을 위해 마지막 예제를 살펴보고 마치도록 하겠다.
new 연산자를 이용 할 때 이다.
function b(callback) {
callback()
}
function A() {
b(()=>{console.log(this)})
}
A() // 지지고 볶아도 항상 window와 연결된다.
var c = new A(); new 연산자는 객체를 만들어 그 객체에 this를 바인딩한다. Aa6 객체
new 연산자를 사용하지 않는 경우에는 this가 window와 바인딩 되어있다. 정의 시점에서 A는 프로토타입일뿐 객체가 아니기때문이다.
그러나 new 연산자를 사용하게되고 생성자를 호출하면 처음 정의될 당시에는 window로 묶여있었으나 new 연산자가 객체를 만들고 this를 바인딩시켜버렸기때문에, 화살표 함수도 this가 A객체로 바인딩 된 것이다.
이 포스트 첫부분에서 기존의 방법과 구분하기 위해 정의될때를 바라보자고 했었는데,
면밀히 이야기하자면 정의시점이 아니라, 호출시점에서 바인딩을 한다는 개념은 같지만 그 대상이 호출한 대상이 아니라 호출 문맥을 바라본다는 것을 이해하였음 한다.
댓글
댓글 쓰기