⚠️ 여기서 설명하는 this
는 JS 안에서의 개념이다. 다른 언어에서도 this
를 사용하지만, 똑같은 개념으로 보는 일이 없도록 하자.
Confusions
this
는 그 이름 때문에 개발자들에게 종종 혼동을 가져다 준다. 이 개념에 대해 크게 착각하는 두 가지가 있는데, 그것은 다음과 같다.
this
는 선언된 함수 자신을 가리킨다.this
는 호출한 함수의 스코프를 가리킨다.
이러한 착각은 주로 this
를 렉시컬 스코프의 개념과 연관지어 추론할 때 발생하고는 한다.
What's this?
바인딩 = binding = 묶는, 결속하는 행위
this
는 동적으로 무언가를 바인딩한다. 즉, 함수 인용 조건에 따른 맥락 의존적인 성격을 가지고 있는 개념으로 보는 게 더 정확하다. 그렇기 때문에 this
는 함수가 어디서 선언되었는지가 아닌, 어디서, 어떻게 호출되었는지에 따라 바인딩하는 주체가 바뀐다.
함수가 인용되었을 때, '실행 컨텍스트'로 더 잘 알려진 activation record가 생성된다. (직역하면 활성화 기록이라고는 하는데, 정확한 단어인지는 모르겠다) 아무튼 이 기록 상에서는 함수가 호출 스택 상에서 어디서, 어떻게 호출 되었는지, 어떤 매개변수가 통과되었는지 등 각종 런타임 정보를 갖고 있기 때문에, 이 안에서 해당 함수가 실행되는 동안 참조할 this
의 레퍼런스 값을 보존하고 있는 것이다.
Call-site
call-site(호출 지점)란, 코드 상에서 함수가 호출된 지점을 얘기한다. 이것을 제대로 파악하기 위해서는 call-stack(호출 스택)을 먼저 아는 것이 중요하다. 자료구조 중 stack의 개념을 차용해 어떤 함수를 순서대로 호출하게 되면 호출된 함수가 계속 쌓이면서 LIFO의 입출력을 이루게 되는데, 이것을 call-stack 이라고 한다.
call-stack 상에서 함수가 연이어 호출될 때는 자연스레 호출을 기준으로 주종 관계가 형성 되고 한 지점에서 함수를 호출했을 때는 호출된 함수 시점에서 그 지점이 call-site가 된다.
Nothing But Rules
Default Binding
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
전역에 선언한 var a = 2;
는 global
object의 프로퍼티가 된다. 그리고 foo()
에 대한 call-site가 전역이므로 this
는 자연스레 전역을 바인딩하는 것이다.
⚠️ in strict mode
전역 혹은 this
가 선언된 스코프에 'use strict'
가 선언되어 엄격 모드가 활성화 될 경우, 일반 함수의 this
는 아무것도 참조할 수 없어 undefined
상태가 된다. 이에 대해 개인적으로 찾아본 이유는 다음과 같다. ECMA5 전까지 생성자 함수 패턴을 사용하는 개발자들이 new
키워드를 쓰지 않을 경우 this
의 예상 값이 적절치 않아 이에 대한 혼동 우려를 방지할 대책이 필요했는데, 그에 대한 방법 중 하나로 엄격 모드에서는 전역을 바인딩하지 않도록 하는 대안을 제시했다.
//non strict mode
function myConstructor(){
this.a = 2;
this.b = 3;
}
var myInstance = new myConstructor(); //this는 새로 생성된 인스턴스를 바인딩한다.
var myBadInstace = myConstructor(); //global 객체에 프로퍼티가 생성된다.
Implicit Binding
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
해당 코드에서 foo()
함수는 obj
의 메소드로 위치하고 있다. 이러한 경우 obj
를 foo()
에 대한 포함, 소유 객체라고 말할 수 있는데, 사실 실질적으로 obj
가 해당 함수를 포함하거나 소유하는 것은 아니다. 다만 foo()
에 대한 call-site가 foo()
를 참조하기 위해 obj
의 컨텍스트를 사용하고 있는 것이므로 함수가 호출되는 시점만큼은 obj
가 함수를 가지고 있다고 표현할 수 있는 것이다.
때문에 함수 참조에 대한 컨텍스트 객체가 존재할 때, 함수 안에서 선언된 this
가 바인딩하는 주체는 해당 객체가 되고 이러한 방식을 '암시적 바인딩'이라고 한다. 이 암시적 바인딩은 객체 참조 체인이나 콜백으로 함수를 넘겨주게 되는 경우 혼동을 야기할 수 있는데, 이는 해당 함수의 call-site가 어떤 context와 직접적으로 연결되어 있는지만 잘 생각하면 된다.
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// `fn` is just another reference to `foo`
fn(); // <-- call-site!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` also property on global object
doFoo( obj.foo ); // "oops, global"
콜백으로 넘겨주었을 때 헷갈릴 수 있는 예시지만, 이렇게 생각해 볼 수 있다.
obj
의foo
프로퍼티는 동명의 함수를 '참조'하는 것이지, 실질적인 값을 가지는 게 아니다.- 때문에
doFoo
함수에 넘겨주는 인자는 선언부의foo()
함수와 같다. - 해당 함수를 호출하는 call-site가 참조를 위해 사용하는 객체가 따로 없기에 결국 default binding 과 같은 결과를 갖게 된다.
이러한 현상은 생성자 함수로 만들어낸 instance 에도 동일하게 적용된다. instance도 결국 객체이기 때문이다.
Explicit Binding
위의 암시적 바인딩과는 달리 함수 내부에 존재하고 있는 api인 bind
, call
, apply
를 사용하면 좀 더 직접적으로 바인딩이 가능하다. 각 메소드는 인자로 받는 값들에 따라 실행하는 동작들이 약간 달라질 뿐, this
의 직접적인 바인딩을 위한다는 성격은 모두 같다. 그 예시로 apply
의 hard binding 형태로 출연한 것이 bind
함수다.
bind
의 경우, 바인딩된 타겟 함수의 이름을.name
프로퍼티로 갖게 되는데, 만약a
라는 변수에 바인드 함수를 써서 할당을 시키고a.name
을 참조하면 "bound [target]" 이라는 이름을 갖게 된다.
추가로 forEach()
나 map()
처럼 기존 JS의 라이브러리 함수같은 경우, 콜백 인자 다음으로 인자를 넣어주게 되면 두번 째 인자를 바인딩해준다. 이는 최근에 안 사실인데, 나중에 시도해보면 좋을 것 같다.
new
Binding
JS 에서는 다른 클래스 지향 언어의 특징처럼 new
연산자를 가지고 있다. 하지만 이것은 겉보기에만 유사한 것이지, 실제 클래스 지향적인 기능과는 전혀 상관 없다. 그리고 JS의 constructor
는 단지 함수일 뿐이다. 즉, constructor
라는 명칭이 따로 있지만 결국 regular function과 동일한 성질을 가지고 있다. 약간 다른 점이 있다면 이 함수의 호출 방식은 '생성자 호출'을 따른다. new
를 동반한 호출은 다음과 같은 특징을 가진다.
- 호출되는 함수가 마땅한 객체를 반환하지 않더라도 새로운 객체가 생성되고, 이 객체는 프로토타입을 가진다.
- 새로운 객체는 호출된 함수의
this
에 바인딩 된다. (implicit binding)
Everything In Order
지금까지 bind
에 대한 4가지 rule을 알아 보았다. 하지만 만약, this
의 주체를 알아야하는 상황에서 두 가지 이상의 rule이 중첩될 때 어떻게 우선 순위를 구할 수 있을까? 본문에서 예시를 보여주지만 간단히 정리하면 순서는 다음과 같다.
new
binding- explicit binding :
call
,apply
+bind
- implicit binding
- default binding : strict vs non-strict mode
앞으로 this
를 마주하게 되면, 위의 결과를 토대로 판단해주면 된다.
Lexical this
일반 함수에 대한 this
판단 방식이 동적으로 결정된다면, 화살표 함수에서는 오직 lexical rule에 기반해서 상위에 근접한 함수 호출 주체를 this
로 설정한다. 그렇기 때문에 화살표 함수 안의 this
를 판별하기 위해서 실행 시점보다는 선언 시점을 파악하는 것이 더 정확한 방법이다.
하지만 this-style code 를 작성할 때, 제대로 된 이해없이 화살표 함수나 var self = this
를 쓰는 등 여러 트릭을 지향하게 되면 유지 보수가 점점 어려워지기 때문에 클린 코드를 위해서는 올바른 이해와 일관된 사용 방법 등이 권장된다.
- 정말
this
가 필요한 케이스가 아니라면, lexical scope rule 로 커버할 것 this
를 정확하게 이해하고, 필요한 곳은bind(..)
로 직접 바인딩해서 가독성을 높일 것this
관련 요행은 자제하자. (화살표 함수,self
etc.)
'Development' 카테고리의 다른 글
[21.01.29] YDKJSY - Mixing (Up) "Class" Objects (0) | 2024.01.07 |
---|---|
[21.01.22] YDKJSY - Objects (0) | 2024.01.07 |
[21.01.16] YDKJSY - Appendix A: Exploring Further (1) | 2024.01.07 |
[21.01.13] YDKJSY - The Module Pattern (0) | 2024.01.07 |
[21.01.11] YDKJSY - Using Closures (1) | 2024.01.07 |