이 글은 You Don't Know JS - this와 객체 프로토타입, 비동기와 성능 책을 읽고 정리한 내용입니다.
0. 서론
this는 기본적으로 호출부(함수가 어떻게 호출됐는가?)에서 함수를 호출할 때 바인딩 된다. 즉, 함수 호출이 이뤄지는 부분을 확인해야 '현재 context에서 this가 가리키는 것'이 무엇인지 확실하게 알 수 있는 것이다. 단순히 '함수를 호출한 지점'으로 돌아가면 호출부를 확인할 수 있다고 생각할 수 있지만, 코드의 복잡도에 따라 '진짜' 호출부를 찾는 것은 모호할 때가 많다. 따라서, 중요한 것은 호출 스택(현재 실행 지점에 오기까지 호출된 함수의 스택)을 생각해보는 것이다. 이 중 호출부는 현재 실행 중인 함수 '직전'의 호출 코드 '내부'에 있다.
function first() {
// 호출 스택: first
// 현재 호출부: 전역 스코프 내부
console.log("first");
secondFunc();
}
function second() {
// 호출 스택: first -> second
// 현재 호출부: first 내부
console.log("second");
third();
}
function third() {
// 호출 스택: first -> second -> third
// 현재 호출부: second 내부
console.log("third");
}
first(); // <- first의 호출부
1. this 바인딩 규칙
1) 기본 바인딩
- 일반적인 함수 호출시
- this: 전역 객체 (엄격 모드시 undefined)
함수의 호출부에서 어떠한 처리도 없이 기본적인 함수 레퍼런스(함수 콜)로 호출되었을 때, 기본 바인딩을 따르게 된다.
기본 바인딩은 엄격 모드(use strict)의 경우 undefined를 가리키게 되며, 아닐 경우 무조건 전역 객체를 의미하게 된다.
// 기본 바인딩
function func() {
console.log(this.val);
}
var val = 3;
func(); // 출력: 3
// 기본 바인딩 - use strict
function func() {
'use strict';
console.log(this.val);
}
var val = 3;
func(); // 출력: Uncaught TypeError: Cannot read properties of undefined (reading 'val')
2) 암시적 바인딩
- 특정 Object가 함수를 프로퍼티로서 참조할 시
- this: 함수를 소유/포함하고 있는 Object
호출부에 콘텍스트 객체가 있을 경우, 해당되는 바인딩 규칙이다. 즉, 객체가 호출된 함수를 소유/포함하고 있는 경우이다. 아래 예시에서 보면, obj 객체는 func() 함수를 프로퍼티로서 참조하고 있다. 따라서 obj.func()을 실행하였을 때는, func() 함수는 자신을 호출한 객체인 obj를 this로 바인딩하게 된다. 즉, this가 obj인 상태이므로 함수 내부에서 호출하는 this.a는 obj.a가 되는 것이다.
// 암시적 바인딩
function func() {
console.log(this.val);
}
var obj = {
val: 2,
func: func
}
obj.func(); // 출력: 2
이런 암시적 바인딩에서 빠지기 쉬운 함정이 있는데, 바로 암시적 소실이다.
암시적 소실이란 암시적 바인딩 된 함수를 다른 변수가 참조하도록 하면, 원래 바인딩이 소실되고 기본 바인딩이 적용되는 것을 의미한다. 코드를 통해 예제를 보자.
// 암시적 소실
function func() {
console.log(this.val);
}
var obj = {
val: 2,
func: func
}
var ref = obj.func;
var val = '전역입니다^^';
ref(); // 출력: 전역입니다^^
위 코드에서 보면 obj.func()에서 func()은 obj에 바인딩 되어 있으므로 ref()를 호출하면 obj.func()이 정상적으로 실행되리라 생각이 되지만, 그렇지 않다. 그 이유는 ref = obj.func는 func의 레퍼런스만을 할당해주는 코드이기 때문이다. 즉, 위 코드에서 ref()를 호출하는 것은 obj.func()를 호출하는 것이 아니라, 기본 func()를 호출하는 것이다. 따라서 obj에 암시적 바인딩 되지 않은, 기본 바인딩 된 func()의 호출로, 콘솔창엔 "전역입니다^^"가 찍히게 된다.
3) 명시적 바인딩
- call(객체), apply(객체), bind(객체)
- call, apply, bind의 파라미터로 넘어온 객체
명시적 바인딩은 말 그대로, call(), apply() 함수를 사용하여 바인딩할 객체를 명시해주는 것을 의미한다(bind()의 경우 엄연히 말하면 하드 바인딩으로 조금 다르다. 뒤에 설명한다). 코드를 통해 설명할 수 있다.
// 명시적 바인딩
function func() {
console.log(this.val);
}
var obj = {
val: 2
}
func.call(obj); // 출력: 2
func.apply(obj); // 출력: 2
위에서 bind()함수의 경우 하드 바인딩이라고 언급했다. 하드 바인딩은, 해당 함수를 특정 객체에 바인딩한 새로운 함수를 반환해주어 변수에 저장할 수 있게끔 하는 것이다. 따라서 하드 바인딩을 하게 되면, 일회성의 call(), apply() 함수와는 다르게 아예 바인딩 된 함수를 가져와 변수처럼 사용할 수 있게된다.
// 하드 바인딩 - 바인딩 된 새로운 함수를 가져온다!
function func() {
console.log(this.val);
}
var obj = {
val: 2
}
var bound = func.bind(obj); // 아예 obj에 바인딩 된 func()를 만들자!
bound(); // 출력: 2;
하드 바인딩의 또 다른 사용이유로, 이미 하드 바인딩 된 함수는 call()과 apply()를 활용해 다시 바인딩 할 수 없다는 점이 있다.
// 하드 바인딩 - 다시 바인딩 할 수는 없다!
function func() {
console.log(this.val);
}
var obj = {
val: 2
}
var bound = func.bind(obj); // 나 하드 바인딩 했다!
bound(); // 출력: 2
var obj2 = { // 새로 바인딩 시도할 객체
val: 10
}
bound.call(obj2); // 출력: 2
위 코드에서 obj로 하드 바인딩 된 bound() 함수를 다시 obj2로 명시적 바인딩하더라도, 절대 바인딩되지 않는다. 이는 써드 파티 라이브러리를 활용해서 쓰거나 다수의 개발자들이 협업을 하는 경우에, 바인딩이 꼬이지 않도록 해줄 수 있다.
4) new 바인딩
- new 함수();
- this: 새로이 생성된 함수 객체
기본적으로, 자바스크립트에서 new는 다른 언어와 같은 '생성자 함수'가 아니라 '함수를 생성하는 호출 방식'이다. new 키워드를 사용해서 함수를 호출하게 되면, 함수 자체가 객체를 생성해내게 되는 것이며, 아래와 같은 로직으로 동작한다.
a. 새 객체가 생성됨
b. 새로 생성된 객체의 [[Prototype]]이 연결됨
c. 새로 생성된 객체는 해당 함수 호출 시 this로 바인딩 됨.
d. 이 함수가 자신의 또 다른 객체를 반환하지 않는 한 new와 함께 호출된 함수는 자동으로 새로 생성된 객체를 반환함.
// new 바인딩
function func(val) {
this.val = val;
}
var obj = new func(2);
console.log(obj.val);
5) 화살표 함수에서의 바인딩
- () => {}
- this: 렉시컬 스코프의 this(함수 또는 전역)
화살표 함수의 경우, 자체적인 this를 절대 가질 수 없다. 다만, 자신이 속한 현재 렉시컬 스코프의 this 값이 사용된다.
function func() {
// 화살표 함수를 return
return a => {
console.log(this.a);
};
}
var obj = {
a: 2
};
var obj2 = {
a: 3
}
var arrowFunc = func.call(obj);
arrowFunc.call(obj2); // 출력: 2
위 코드에서 보면, func()로부터 반환된 화살표 함수를 call(obj2)를 통해 obj2에 명시적 바인딩했지만, 이미 arrowFunc 자체가 obj를 바인딩한 func()에서 반환된 화살표 함수이므로 obj의 a 값이 출력되는 것을 확인할 수 있다. 이처럼, 화살표 함수는 자체적인 바인딩이 불가능하며, 그 자신의 렉시컬 스코프를 따라간다.
2. this 바인딩 규칙 적용 우선순위
1) new 바인딩 - 새로 생성된 객체
2) 명시적 바인딩(call, apply, bind) - 명시적으로 지정된 객체
3) 암시적 바인딩 - 콘텍스트 객체
4) 기본 바인딩
'Language > JS' 카테고리의 다른 글
[Javascript] KeyboardEvent.key / 키보드 이벤트 (0) | 2021.08.31 |
---|---|
[Javascript] 자바스크립트 문자열 뒤집기 (0) | 2021.06.15 |
[Javascript] 디바운싱과 쓰로틀링 / 연속 이벤트 제어 (0) | 2021.06.08 |
[Javascript] Map 사용법 (0) | 2021.06.08 |
[Javascript] 일반 함수 vs 익명 함수 (4) | 2021.05.28 |