화살표 함수의 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 이다.


3. 동작 원리

3.1 this 를 찾는 경로

1장의 화살표 콜백이 this.count 를 읽는 과정이다. forEach 가 콜백을 세 번 호출하지만, 콜백 환경에는 this 가 없으므로 매번 addAll 의 환경에서 this 를 읽는다.

화살표 함수의 this 탐색 경로
그림 1. 일반 함수 콜백은 호출될 때 자기 환경에 this 를 만든다(forEach 가 thisArg 없이 호출하면 undefined). 화살표 콜백의 환경에는 this 가 없어 바깥 addAll 환경의 this 를 읽는다.

인터랙티브 다이어그램: counter.addAll([1, 2, 3]) 에서 화살표 콜백의 this (단계별로 재생할 수 있다)

  1. counter.addAll(...) 을 메서드로 호출한다. addAll 의 환경에 this = counter 가 바인딩된다.
  2. forEach 가 첫 번째 요소로 화살표 콜백을 호출한다. 콜백 환경에는 this 바인딩이 만들어지지 않는다.
  3. this.count 를 평가한다. 콜백 환경에 this 가 없어 바깥 addAll 환경의 this(counter)를 읽는다.
  4. 콜백이 끝나면 환경과 실행 컨텍스트가 함께 제거된다. 두 번째, 세 번째 호출도 같은 경로로 this 를 읽는다.
  5. 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. [1] 화살표 함수 표현식 — MDN Web Docs
  2. [2] OrdinaryFunctionCreate — ECMAScript 사양
  3. [3] GetThisEnvironment — ECMAScript 사양

관련 글