화살표 함수의 this 와 일반 함수와의 차이
화살표 함수는 자체 this 바인딩을 만들지 않는다. 그래서 this 를 정의된 위치의 바깥 함수에서 찾고, call·bind 로도 바꿀 수 없으며, new 로 호출할 수 없다.
Seobway · · 7분
1. 문제 상황
this 바인딩 규칙에서 본 것처럼 일반 함수의 this 는 호출 방식으로 정해진다. 콜백으로 넘긴 일반 함수는 메서드 형태로 호출되지 않으므로 바깥 메서드의 this 를 쓸 수 없다.
const counter = {
count: 0,
addAll(items) {
items.forEach(function () {
this.count++; // this 는 counter 가 아니다
});
return this.count;
},
};
counter.addAll([1, 2, 3]); // 0
콜백을 화살표 함수로 바꾸면 결과가 3이 된다.
items.forEach(() => {
this.count++; // addAll 의 this, 즉 counter
});
두 콜백은 문법만 다른 것이 아니라, 호출될 때 this 를 다루는 방식이 다르다.
2. 정의
2.1 정의
화살표 함수는 (매개변수) => 본문 형태의 함수 표현식이다. 일반 함수와 같은 함수 객체지만, 호출될 때 환경 레코드에 this 를 바인딩하지 않는다. 함수 안에서 this 를 읽으면 스코프 체인을 따라 바깥으로 올라가 this 를 가진 첫 환경에서 값을 읽는다.
일반 함수와의 차이는 다음과 같다.
| 항목 | 일반 함수 (function) |
화살표 함수 |
|---|---|---|
this |
호출 방식으로 결정 | 정의된 위치의 바깥 this |
call·apply·bind 의 첫 인자 |
this 가 된다 |
무시된다 |
arguments |
있음 | 없음 (바깥 함수의 것을 읽는다) |
prototype 프로퍼티 |
있음 | 없음 |
new 호출 |
가능 | TypeError: … is not a constructor |
2.2 사양에서의 위치
ECMAScript 사양은 함수 객체를 만들 때 내부 슬롯 [[ThisMode]] 를 정한다[2]. 화살표 함수는 lexical, 엄격 모드의 일반 함수는 strict, 그 외는 global 이다.
- 호출 시 함수 환경 레코드를 만들 때
[[ThisMode]]가lexical이면 그 환경은this바인딩을 갖지 않는다. this를 평가하는GetThisEnvironment는 현재 환경부터 바깥으로 올라가며this바인딩을 가진 첫 환경을 찾는다[3]. 화살표 함수의 환경은 건너뛰어진다.OrdinaryCallBindThis는[[ThisMode]]가lexical이면 아무것도 바인딩하지 않고 끝난다.call의 첫 인자가 쓰일 곳이 없다.- 화살표 함수를 만드는 과정에서는
MakeConstructor를 호출하지 않는다. 그래서 [[Construct]] 와prototype이 없다.
3. 동작 원리
3.1 this 를 찾는 경로
1장의 화살표 콜백이 this.count 를 읽는 과정이다. forEach 가 콜백을 세 번 호출하지만, 콜백 환경에는 this 가 없으므로 매번 addAll 의 환경에서 this 를 읽는다.
인터랙티브 다이어그램: counter.addAll([1, 2, 3]) 에서 화살표 콜백의 this (단계별로 재생할 수 있다)
- counter.addAll(...) 을 메서드로 호출한다. addAll 의 환경에 this = counter 가 바인딩된다.
- forEach 가 첫 번째 요소로 화살표 콜백을 호출한다. 콜백 환경에는 this 바인딩이 만들어지지 않는다.
- this.count 를 평가한다. 콜백 환경에 this 가 없어 바깥 addAll 환경의 this(counter)를 읽는다.
- 콜백이 끝나면 환경과 실행 컨텍스트가 함께 제거된다. 두 번째, 세 번째 호출도 같은 경로로 this 를 읽는다.
- forEach 와 addAll 이 끝나고 3 이 반환된다.
3.2 call·bind 로 바꿀 수 없는 이유
call 은 함수의 [[Call]] 을 실행하면서 첫 인자를 this 후보로 넘긴다. 화살표 함수는 OrdinaryCallBindThis 단계에서 바인딩을 하지 않으므로 그 값이 버려진다. bind 로 만든 함수도 결국 같은 경로로 호출하므로 결과가 같다.
function outer() {
const f = () => this.label;
return f.call({ label: "call 인자" });
}
outer.call({ label: "outer 의 this" }); // "outer 의 this"
arguments 도 같은 방식이다. 사양은 [[ThisMode]] 가 lexical 인 함수에 arguments 객체를 만들지 않는다. 화살표 함수 안의 arguments 는 바깥 일반 함수의 arguments 를 가리킨다. 화살표 함수에서 인자 전체가 필요하면 나머지 매개변수 (...args) => 를 쓴다.
4. 예제
4.1 기본
같은 객체에서 일반 함수 콜백, 화살표 콜백, call 로 this 를 바꾼 화살표 함수를 비교한다. 실행 전에 각 줄의 출력을 예측해 본다.
"use strict";
const counter = {
count: 0,
withFunction(items) {
items.forEach(function () {
// 이 함수의 this 는 forEach 가 넘긴 thisArg(없으면 undefined)
if (this) this.count++;
});
return this.count;
},
withArrow(items) {
items.forEach(() => { this.count++; }); // withArrow 의 this
return this.count;
},
};
console.log("일반 함수 콜백 →", counter.withFunction([1, 2, 3]));
console.log("화살표 콜백 →", counter.withArrow([1, 2, 3]));
function outer() {
const f = () => this.label;
return f.call({ label: "call 인자" });
}
console.log("arrow.call(…) →", outer.call({ label: "outer 의 this" }));
const arrow = () => {};
console.log("prototype 있음? →", "prototype" in arrow);
try {
new arrow();
} catch (e) {
console.log("new arrow() →", e.constructor.name, e.message);
}
결과는 0, 3, "outer 의 this", false, TypeError 순서다. Node 22 에서 실행하면 같은 값이 나온다.
4.2 실무 패턴
클래스 필드에 화살표 함수를 대입하면, 메서드를 떼어 내 콜백으로 넘겨도 this 가 유지된다. React 클래스 컴포넌트의 이벤트 핸들러가 이 형태를 썼다.
class Timer {
seconds = 0;
tick = () => { this.seconds++; }; // 인스턴스마다 새 함수, this 는 인스턴스
start() {
setInterval(this.tick, 1000); // 떼어 내도 this 가 유지된다
}
}
클래스 필드 초기화 코드는 생성자 안에서 실행되므로, 화살표 함수의 바깥 this 는 만들어지는 인스턴스다. 대신 이 함수는 prototype 에 한 번 정의되는 메서드와 달리 인스턴스마다 하나씩 만들어진다(new Timer().tick === new Timer().tick 은 false).
5. 자주 하는 실수
6. 정리
화살표 함수는 [[ThisMode]] 가 lexical 인 함수다. 호출될 때 this 와 arguments 를 만들지 않으므로 둘 다 바깥 함수에서 읽고, call·bind 의 첫 인자는 무시된다. [[Construct]] 가 없어 new 로 호출할 수 없다.
- 1장 코드에서 일반 함수 콜백이
0을 반환하는 이유를 설명할 수 있다 - 화살표 함수의
this가 어느 환경에서 읽히는지 말할 수 있다 -
call로 화살표 함수의this를 바꿀 수 없는 이유를 안다 - 객체 메서드와 DOM 핸들러에 화살표 함수를 쓰면 안 되는 경우를 안다
- 화살표 함수에서 객체 리터럴을 반환하는 문법을 안다
참고
- [1] 화살표 함수 표현식 — MDN Web Docs
- [2] OrdinaryFunctionCreate — ECMAScript 사양
- [3] GetThisEnvironment — ECMAScript 사양
관련 글
- this 바인딩 규칙 → (선행 개념)
- 실행 컨텍스트의 구성과 생성 과정 →
- 고차 함수와 콜백 → (화살표 함수를 콜백으로 넘기는 함수)
- javascript
- arrow-function
- this
- arguments
- 화살표 함수