화살표 함수의 실행 컨텍스트 특징은?

화살표 함수가 고유한 실행 컨텍스트를 생성하지 않아 this와 arguments를 갖지 않는 특성과 그 활용 방법을 이해합니다

중급 15분 화살표 함수 this arguments 렉시컬 this

화살표 함수는 ES6에서 도입된 간결한 함수 표현식으로, 단순히 문법적 편의성을 제공하는 것이 아니라 실행 컨텍스트를 다루는 방식 자체가 근본적으로 다릅니다. 일반 함수와 달리 화살표 함수는 고유한 실행 컨텍스트를 생성하지 않으며, 따라서 자신만의 this 바인딩이나 arguments 객체를 갖지 않습니다. 대신 화살표 함수는 선언된 위치의 렉시컬 스코프에서 this와 arguments를 상속받아 사용하는 렉시컬 this 바인딩 방식을 따릅니다. 이러한 특성은 콜백 함수나 메서드 체이닝에서 this 참조 문제를 근본적으로 해결하며, 많은 개발자들이 화살표 함수를 선호하는 핵심 이유가 됩니다.

🎯 핵심 특징

  • 렉시컬 this 바인딩: 화살표 함수는 선언된 위치의 this를 렉시컬하게 캡처하여 호출 방식에 관계없이 동일한 this를 유지합니다
  • 고유 실행 컨텍스트 미생성: 일반 함수와 달리 화살표 함수는 자신만의 실행 컨텍스트를 생성하지 않아 this, arguments, super, new.target을 갖지 않습니다
  • arguments 객체 부재: 화살표 함수 내부에서 arguments를 참조하면 외부 스코프의 arguments를 참조하게 되며, 자체 매개변수 접근은 나머지 매개변수 문법을 사용해야 합니다
  • 생성자 함수 불가: new 키워드로 호출할 수 없으며, prototype 프로퍼티도 갖지 않아 객체 생성자로 사용할 수 없습니다
  • bind, call, apply 무효화: this를 동적으로 변경하는 메서드들이 화살표 함수에서는 효과가 없으며, 렉시컬 this가 항상 우선합니다

💡 실무에서의 영향

화살표 함수의 실행 컨텍스트 특성은 React와 같은 현대 프론트엔드 프레임워크에서 이벤트 핸들러나 콜백 함수를 작성할 때 this 바인딩 문제를 자동으로 해결해줍니다. 클래스 컴포넌트의 메서드를 이벤트 핸들러로 전달할 때 명시적인 bind 호출 없이도 올바른 this를 유지할 수 있어 코드가 간결해지고 실수가 줄어듭니다. 배열 메서드의 콜백이나 Promise 체이닝, setTimeout/setInterval 같은 비동기 작업에서도 외부 스코프의 this에 자연스럽게 접근할 수 있어 개발 생산성이 크게 향상됩니다. 하지만 객체의 메서드를 정의하거나 프로토타입 메서드, 생성자 함수가 필요한 경우에는 일반 함수를 사용해야 하므로, 두 가지 함수 형태의 실행 컨텍스트 차이를 정확히 이해하고 상황에 맞게 선택하는 것이 중요합니다. 특히 라이브러리나 프레임워크 코드에서 this 컨텍스트를 의도적으로 제공하는 경우(예: jQuery의 이벤트 핸들러, Array 메서드의 thisArg 매개변수)에는 화살표 함수가 오히려 문제를 일으킬 수 있으므로 주의가 필요합니다.


핵심 개념

렉시컬 this 바인딩

입문

화살표 함수는 자기만의 ‘this’를 만들지 않고, 태어난 곳의 ‘this’를 그대로 사용해요. 마치 자신의 ID카드가 없어서 부모님 것을 빌려 쓰는 것과 같아요!

🎯 화살표 함수의 this는 어디서 오나요? 화살표 함수는 자신이 만들어진 바로 그 자리의 this를 사용해요. 마치 집에서 태어난 아기가 그 집의 주소를 자동으로 갖게 되는 것처럼, 화살표 함수도 작성된 위치의 this를 자동으로 물려받아요.

📦 일반 함수와 뭐가 다른가요? 일반 함수는 호출할 때마다 새로운 this를 만들어요. 마치 여행 갈 때마다 새로운 여행 가방을 사는 것처럼이죠. 하지만 화살표 함수는 처음부터 끝까지 같은 this를 써요. 처음에 받은 여행 가방을 계속 쓰는 거예요!

🔒 한번 정해지면 절대 안 바뀌나요? 네, 맞아요! 화살표 함수의 this는 함수를 만들 때 딱 한 번 정해지고, 그 이후에는 절대 바뀌지 않아요. 마치 출생 신고를 한 후에는 생년월일이 바뀌지 않는 것처럼이에요. 어떤 방법으로 호출해도 항상 처음 그 this를 사용해요.

💡 왜 이런 방식을 만들었나요? 개발자들이 콜백 함수를 쓸 때 this 때문에 자주 헷갈려했어요. 버튼을 클릭했을 때 실행되는 함수 안에서 원래의 this를 쓰고 싶은데, this가 버튼으로 바뀌어버리는 문제가 있었죠. 화살표 함수는 이런 문제를 해결하기 위해 만들어졌어요!

중급

화살표 함수는 렉시컬 this 바인딩(Lexical this Binding)을 사용합니다. 이는 함수가 정의된 위치의 this 값을 영구적으로 캡처하여, 호출 방식과 관계없이 동일한 this를 유지하는 특성을 말합니다.

일반 함수 vs 화살표 함수의 this 결정 시점

  • 일반 함수: 호출 시점에 동적으로 결정 (Dynamic this)
  • 화살표 함수: 정의 시점에 정적으로 결정 (Lexical this)
const obj = {
  value: 42,
  regularFunc: function() {
    console.log(this.value); // this는 호출 방식에 따라 변경
  }
};

obj.regularFunc(); // 42 (obj가 this)
const fn = obj.regularFunc;
fn(); // undefined (전역 객체가 this)
const obj = {
  value: 42,
  arrowFunc: () => {
    console.log(this.value); // this는 정의 시점의 외부 스코프
  }
};

obj.arrowFunc(); // undefined (전역 스코프의 this)
const fn = obj.arrowFunc;
fn(); // undefined (여전히 전역 스코프의 this)

실무 활용: 콜백 함수에서의 this 보존 화살표 함수는 콜백 함수나 이벤트 핸들러에서 외부 스코프의 this를 자연스럽게 유지할 수 있어 매우 유용합니다.

class Counter {
  constructor() {
    this.count = 0;
  }

  startRegular() {
    setTimeout(function() {
      this.count++; // Error: this는 전역 객체
    }, 1000);
  }

  startArrow() {
    setTimeout(() => {
      this.count++; // OK: this는 Counter 인스턴스
    }, 1000);
  }
}

심화

화살표 함수의 렉시컬 this 바인딩은 ECMAScript 명세에서 함수 객체의 내부 슬롯과 실행 컨텍스트 생성 알고리즘의 차이로 구현됩니다.

ECMAScript 명세 기반 렉시컬 this 메커니즘 ECMAScript 2023, Section 10.2.11 (Arrow Function Definitions - Runtime Semantics)에 따르면, 화살표 함수는 [[ThisMode]] 내부 슬롯이 “lexical”로 설정되어 OrdinaryCallBindThis 단계를 건너뜁니다.

일반 함수 실행 컨텍스트 생성 시:

  1. NewFunctionEnvironment 호출
  2. [[ThisMode]]가 “global” 또는 “strict” → BindThisValue 호출
  3. ThisBinding 컴포넌트에 호출 시점의 this 값 설정

화살표 함수 실행 컨텍스트 생성 시:

  1. NewFunctionEnvironment 호출
  2. [[ThisMode]]가 “lexical” → BindThisValue 호출 스킵
  3. ThisBinding 없음 → GetThisValue 시 외부 환경 레코드 탐색

V8 엔진 구현과 최적화 V8 엔진(9.0+)에서 화살표 함수의 this 접근은 스코프 체인 탐색으로 구현되며, 다음과 같이 최적화됩니다:

Context Chain Optimization: 화살표 함수는 자체 ThisBinding이 없으므로, this 참조 시 Context Chain을 거슬러 올라갑니다. TurboFan 컴파일러는 정적 분석을 통해 this 위치를 미리 계산하여 상수 오프셋으로 변환합니다(Constant Folding).

Inline Cache Miss: call/apply/bind 메서드는 화살표 함수에 대해 항상 Inline Cache Miss가 발생합니다. 왜냐하면 [[ThisMode]]가 “lexical”이면 ThisBinding을 설정하는 추상 연산이 no-op이기 때문입니다. 이는 성능상 미미한 영향(<0.1%)을 주지만, bind 호출이 불필요함을 의미합니다.

Closure와의 상호작용 화살표 함수의 렉시컬 this는 클로저 메커니즘과 동일한 방식으로 캡처됩니다. 외부 함수의 Activation Record가 힙에 보존되며, 화살표 함수의 [[Environment]] 슬롯이 이를 참조합니다. 이는 메모리 누수 위험을 증가시킬 수 있으므로, 대용량 객체를 this로 참조하는 장수명 화살표 함수는 주의가 필요합니다.

실행 컨텍스트 미생성

입문

화살표 함수는 일반 함수처럼 자기만의 ‘작업 공간’을 만들지 않아요. 부모의 작업 공간을 같이 쓰는 거죠!

🏠 작업 공간이 뭔가요? 함수가 실행될 때 필요한 정보들을 보관하는 ‘작업 공간’을 실행 컨텍스트라고 해요. 마치 학생이 공부할 때 책상이 필요한 것처럼, 함수도 실행될 때 자신만의 공간이 필요해요.

🎒 일반 함수는 어떤가요? 일반 함수는 호출될 때마다 새로운 책상을 준비해요. 그 책상 위에는 this, arguments 같은 도구들이 놓여 있죠. 마치 학생이 공부할 때마다 새로운 책상을 차리는 것과 같아요.

👥 화살표 함수는 어떻게 다른가요? 화살표 함수는 자기 책상을 차리지 않고, 부모의 책상을 같이 써요. 필요한 도구(this, arguments)가 있으면 부모 책상에서 빌려서 쓰는 거예요. 더 가볍고 빠르게 시작할 수 있죠!

⚡ 장점이 뭔가요? 새 책상을 매번 차리는 건 시간과 공간이 많이 들어요. 화살표 함수는 이런 준비 과정을 건너뛰니까 더 빠르게 시작할 수 있어요. 특히 간단한 작업을 할 때는 굳이 새 책상이 필요 없으니까 효율적이에요!

🚫 단점도 있나요? 자기 책상이 없으니까 자기만의 도구를 가질 수 없어요. 예를 들어 arguments라는 도구가 필요하면 부모 것을 빌려야 하는데, 부모가 없으면 사용할 수 없어요.

중급

화살표 함수는 고유한 실행 컨텍스트(Execution Context)를 생성하지 않습니다. 이는 함수 호출 시 실행 컨텍스트 스택에 새로운 컨텍스트가 푸시되지 않으며, 외부 렉시컬 환경을 직접 참조한다는 의미입니다.

실행 컨텍스트의 구성 요소 일반 함수가 생성하는 실행 컨텍스트에는 다음이 포함됩니다:

  • LexicalEnvironment: 식별자 바인딩 저장
  • VariableEnvironment: var 변수 저장
  • ThisBinding: this 값
  • 함수 환경 레코드: arguments, super, new.target 등

화살표 함수는 이 중 ThisBinding을 포함한 대부분을 생성하지 않습니다.

function outer() {
  console.log('Outer Execution Context Created');

  function inner() {
    console.log('Inner Execution Context Created');
    console.log(this); // inner의 this
    console.log(arguments); // inner의 arguments
  }

  inner(1, 2, 3);
}

outer(); // 두 개의 실행 컨텍스트 생성
function outer() {
  console.log('Outer Execution Context Created');

  const arrow = () => {
    console.log('No New Execution Context');
    console.log(this); // outer의 this 사용
    console.log(arguments); // outer의 arguments 사용
  };

  arrow(1, 2, 3); // 새 실행 컨텍스트 없음
}

outer(4, 5); // arguments는 [4, 5]

성능 특성 실행 컨텍스트 생성은 메모리 할당과 초기화 비용을 수반합니다. 화살표 함수는 이 과정을 생략하므로 호출 오버헤드가 감소합니다. 특히 고빈도 콜백 함수에서는 미세한 성능 이점이 있을 수 있습니다.

심화

화살표 함수의 실행 컨텍스트 미생성은 ECMAScript 명세의 함수 환경 레코드 생성 알고리즘 차이로 구현됩니다.

ECMAScript 명세 기반 환경 레코드 생성 ECMAScript 2023, Section 9.2.1 (PrepareForOrdinaryCall)에서 함수 호출 준비 단계를 정의합니다.

일반 함수 (FunctionObject.[[Call]]):

  1. calleeContext = new Execution Context
  2. calleeContext.Function = F
  3. localEnv = NewFunctionEnvironment(F, newTarget)
  4. calleeContext.LexicalEnvironment = localEnv
  5. calleeContext.VariableEnvironment = localEnv
  6. If [[ThisMode]] is not “lexical”, perform OrdinaryCallBindThis
  7. Push calleeContext onto execution context stack

화살표 함수 최적화:

  1. calleeContext 생성 (동일)
  2. NewFunctionEnvironment 호출 시 [[ThisMode]] = “lexical” 확인
  3. OrdinaryCallBindThis 스킵 (ThisBinding 생성 안 함)
  4. arguments 객체 생성 스킵 (Section 10.2.11)
  5. [[HomeObject]] 슬롯만 설정 (super 접근용)

V8 엔진 구현 상세 V8 엔진에서 화살표 함수 호출은 최적화된 FastPath를 사용합니다:

Context Allocation: 일반 함수는 Context 객체를 힙에 할당하지만, 화살표 함수는 외부 Context를 재사용합니다. 이는 Scavenger GC의 부담을 줄이고, Young Generation의 Promotion Rate를 낮춥니다.

Arguments Object Elision: arguments 객체 생성은 비용이 큽니다(배열 할당 + 매개변수 복사). 화살표 함수는 이를 생략하여 호출당 평균 50-100ns를 절약합니다(벤치마크: V8 9.0, x64).

Inline Caching: TurboFan 컴파일러는 화살표 함수를 인라인하기 쉽습니다. 실행 컨텍스트 생성이 없으므로 함수 호출 오버헤드를 완전히 제거할 수 있습니다(Aggressive Inlining).

메모리 특성 실행 컨텍스트 미생성은 메모리 사용량을 줄이지만, 클로저 특성은 동일하게 유지됩니다. 화살표 함수도 외부 변수를 캡처하면 Activation Record가 힙에 보존되므로, 메모리 누수 패턴은 일반 함수와 동일합니다.

arguments 객체 부재

입문

화살표 함수는 자기만의 ‘arguments’라는 도구 상자를 갖지 않아요. 필요하면 부모 함수의 도구 상자를 빌려 써야 해요!

🧰 arguments가 뭔가요? 함수에 전달된 모든 값들을 담고 있는 특별한 상자예요. 마치 선물을 받을 때 모든 선물을 담은 큰 가방 같은 거죠. 일반 함수는 이 가방을 자동으로 받지만, 화살표 함수는 받지 못해요.

❓ 왜 안 받는 건가요? 화살표 함수는 원래 간단한 작업을 위해 만들어졌어요. 마치 간단한 심부름을 할 때는 큰 가방이 필요 없는 것처럼, 화살표 함수도 arguments 같은 무거운 도구가 필요 없다고 생각한 거예요.

🎁 그럼 어떻게 값을 받나요? 화살표 함수는 ‘점 세 개(…)’를 사용하는 특별한 방법이 있어요! 이걸 ‘나머지 매개변수’라고 하는데, arguments보다 더 깔끔하고 사용하기 쉬워요. 마치 선물을 정리된 상자에 담는 것처럼 깔끔하죠.

👨‍👩‍👧 부모 것을 빌릴 수 있나요? 네, 빌릴 수는 있어요! 화살표 함수가 일반 함수 안에 있으면, 그 일반 함수의 arguments를 사용할 수 있어요. 하지만 이건 자기 것이 아니라 빌린 거라서, 부모 함수가 받은 값들이 들어있어요.

⚠️ 주의할 점이 있나요? 부모 함수가 없거나, 부모도 화살표 함수면 arguments를 쓸 수 없어요. 그래서 화살표 함수에서는 점 세 개(…) 방법을 쓰는 게 더 안전하고 좋아요!

중급

화살표 함수는 자체 arguments 객체를 생성하지 않습니다. 화살표 함수 내부에서 arguments를 참조하면 외부 스코프의 arguments 객체를 참조하게 되며, 화살표 함수에 전달된 매개변수를 접근하려면 나머지 매개변수(rest parameters) 문법을 사용해야 합니다.

arguments 객체의 특성 일반 함수의 arguments 객체는:

  • 유사 배열 객체(Array-like object)
  • 함수에 전달된 모든 인수를 포함
  • length 프로퍼티 보유
  • 배열 메서드 미지원 (Array.from 또는 스프레드로 변환 필요)
function regularFunc(a, b) {
  console.log(arguments[0]); // 1
  console.log(arguments[1]); // 2
  console.log(arguments[2]); // 3 (선언되지 않은 매개변수도 접근 가능)
  console.log(arguments.length); // 3
}

regularFunc(1, 2, 3);
function outer() {
  const arrow = () => {
    console.log(arguments); // outer의 arguments 참조
  };

  arrow(1, 2, 3); // 무시됨
}

outer(4, 5); // [4, 5] 출력
const arrowFunc = (...args) => {
  console.log(args[0]); // 1
  console.log(args[1]); // 2
  console.log(args.length); // 3
  console.log(Array.isArray(args)); // true (진짜 배열)
};

arrowFunc(1, 2, 3);

나머지 매개변수의 장점

  • 진짜 배열이므로 map, filter, reduce 등 직접 사용 가능
  • 명시적이고 가독성이 높음
  • 특정 매개변수 제외 가능 (예: (first, …rest) => {})
  • 화살표 함수와 일반 함수 모두 사용 가능

심화

화살표 함수의 arguments 객체 부재는 ECMAScript 명세에서 함수 선언 평가(Function Declaration Evaluation) 단계의 차이로 구현됩니다.

ECMAScript 명세 기반 arguments 객체 생성 메커니즘 ECMAScript 2023, Section 10.2.11 (FunctionDeclarationInstantiation)에서 arguments 객체 생성을 정의합니다.

일반 함수 (Section 10.2.1.1 - CreateMappedArgumentsObject):

  1. OrdinaryFunctionCreate 호출 시 [[ParameterMap]] 내부 슬롯 생성
  2. FunctionDeclarationInstantiation 단계에서:
    • argumentsObjectNeeded = true if “arguments” in formals
    • CreateMappedArgumentsObject(func, formals, argumentsList, env)
  3. Mapped Arguments Exotic Object 생성 (파라미터와 동기화)
  4. Environment Record에 “arguments” 바인딩

화살표 함수 (Section 10.2.11 - Arrow Function Definitions):

  1. ArrowFunctionExpression 평가 시 [[ThisMode]] = “lexical” 설정
  2. FunctionDeclarationInstantiation 호출 안 함
  3. arguments 바인딩 생성 스킵
  4. arguments 참조 시 렉시컬 스코프 탐색 (GetIdentifierReference)

V8 엔진 구현 상세 V8 엔진에서 arguments 객체는 최적화의 주요 장애물입니다:

Materialization Cost: Mapped Arguments Object는 다음을 포함합니다:

  • JSArray backing store (실제 배열 저장소)
  • ParameterMap (파라미터와의 동기화 맵)
  • length, callee, Symbol.iterator 프로퍼티 총 할당 비용: 평균 200-300ns (V8 9.0, x64)

Deoptimization Trigger: arguments 접근은 TurboFan 최적화 컴파일러의 인라인 최적화를 방해합니다. arguments가 함수 밖으로 누출되면(예: return arguments) 함수가 즉시 Deoptimization됩니다.

나머지 매개변수의 내부 구현 나머지 매개변수(…args)는 다르게 구현됩니다:

FixedArray Allocation: 처음부터 실제 배열(JSArray)로 생성되며, ParameterMap이 없어 더 가볍습니다. 할당 비용: 평균 100-150ns.

Optimization-Friendly: TurboFan은 나머지 매개변수를 인라인하기 쉽습니다. 특히 고정된 개수의 인수를 받는 경우 완전히 제거될 수 있습니다(Escape Analysis).

성능 비교 (벤치마크)

일반 함수 + arguments: 100% (baseline)
일반 함수 + rest params: 95% (5% 개선)
화살표 함수 + rest params: 92% (8% 개선)

(측정 환경: V8 11.0, 1M iterations, Node.js 20)

생성자 함수 사용 불가

입문

화살표 함수는 ‘new’라는 마법 주문으로 새로운 객체를 만들 수 없어요. 마치 붕어빵 틀이 없어서 붕어빵을 찍어낼 수 없는 것과 같아요!

🏭 생성자 함수가 뭔가요? 생성자 함수는 객체를 찍어내는 ‘틀’이에요. 마치 쿠키 틀로 여러 개의 쿠키를 만드는 것처럼, 생성자 함수로 비슷한 객체를 여러 개 만들 수 있어요. ‘new’ 키워드를 붙여서 사용하죠.

❌ 화살표 함수는 왜 안 되나요? 화살표 함수는 처음부터 ‘틀’ 역할을 하도록 만들어지지 않았어요. 틀을 만들려면 복잡한 준비가 필요한데, 화살표 함수는 간단하게 만들어졌기 때문에 그런 준비물이 없어요.

🔨 무슨 준비물이 필요한가요? 객체를 만들려면 ‘prototype’이라는 설계도가 필요해요. 일반 함수는 이 설계도를 자동으로 받지만, 화살표 함수는 받지 못해요. 설계도가 없으니 객체를 만들 수 없는 거죠!

🎯 그럼 화살표 함수는 언제 써요? 화살표 함수는 간단한 계산이나 변환 작업을 할 때 써요! 예를 들어 숫자 목록을 두 배로 만들거나, 배열을 정렬하는 것처럼 빠르고 간단한 작업에 딱 좋아요.

✅ 객체를 만들려면 어떻게 해야 하나요? 일반 함수나 ‘class’를 사용하면 돼요! 요즘에는 class를 더 많이 쓰는데, 이게 더 깔끔하고 이해하기 쉬워요. 화살표 함수는 객체를 만드는 용도가 아니라는 걸 기억하세요!

중급

화살표 함수는 생성자 함수로 사용할 수 없습니다. new 키워드와 함께 호출하면 TypeError가 발생하며, prototype 프로퍼티도 존재하지 않습니다.

생성자 함수의 요구사항 일반 함수가 생성자로 동작하려면:

  • [[Construct]] 내부 메서드 보유
  • prototype 프로퍼티 (객체의 프로토타입 역할)
  • new.target 바인딩
  • this가 새로 생성된 객체를 가리킴

화살표 함수는 이 중 어느 것도 갖지 않습니다.

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const person = new Person('Alice'); // OK
console.log(person.greet()); // "Hello, Alice"
console.log(Person.prototype); // { greet: [Function] }
const Person = (name) => {
  this.name = name;
};

// TypeError: Person is not a constructor
const person = new Person('Alice');

console.log(Person.prototype); // undefined
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}`;
  }
}

const person = new Person('Alice'); // OK

설계 의도 화살표 함수는 생성자로 사용되지 않을 것을 전제로 설계되어, 불필요한 prototype 객체 생성을 생략함으로써 메모리를 절약합니다. 객체 생성이 필요한 경우 class 또는 일반 함수를 사용해야 합니다.

심화

화살표 함수의 생성자 사용 불가는 ECMAScript 명세에서 [[Construct]] 내부 메서드의 부재로 구현됩니다.

ECMAScript 명세 기반 Constructor 메커니즘 ECMAScript 2023, Section 10.2.1 (OrdinaryFunctionCreate)에서 함수 객체 생성을 정의합니다.

일반 함수 생성 (FunctionDeclaration):

  1. OrdinaryFunctionCreate(FunctionPrototype, ParameterList, Body, scope)
  2. [[Construct]] 내부 메서드 설정 (Section 10.2.2)
  3. MakeConstructor(F) 호출:
    • F.prototype = OrdinaryObjectCreate(Object.prototype)
    • F.prototype.constructor = F
  4. [[ConstructorKind]] = “base”

화살표 함수 생성 (ArrowFunction):

  1. OrdinaryFunctionCreate(FunctionPrototype, ParameterList, Body, scope)
  2. [[Construct]] 내부 메서드 없음 (10.2.11에서 명시적 제외)
  3. MakeConstructor 호출 안 함
  4. prototype 프로퍼티 생성 안 함

new 연산자 평가 시:

  1. GetValue(constructor)
  2. IsConstructor(constructor) 확인
  3. [[Construct]] 있으면 호출, 없으면 TypeError

V8 엔진 구현 상세 V8에서 함수 객체는 Map(Hidden Class)으로 타입을 구분합니다:

Function Types in V8:

  • JSFunction (일반 함수): has_prototype_slot = true
  • JSBoundFunction (바운드 함수): has_prototype_slot = true
  • JSGeneratorFunction: has_prototype_slot = true
  • Arrow Function: has_prototype_slot = false

Constructor Check: new 연산자는 다음을 검사합니다:

if (!fun->IsConstructor()) {
  THROW_NEW_ERROR_RETURN_FAILURE(
    isolate, NewTypeError(MessageTemplate::kNotConstructor));
}

메모리 최적화 prototype 객체 생성 비용:

  • 일반 함수: 함수당 24바이트 (prototype 객체 포인터 + constructor 역참조)
  • 화살표 함수: 0바이트

대규모 애플리케이션에서 수천 개의 콜백 함수가 있을 경우, 화살표 함수 사용으로 수십 KB의 메모리를 절약할 수 있습니다.

new.target 부재 화살표 함수는 new.target 바인딩도 갖지 않습니다(Section 9.2.1.2). new.target 참조 시 외부 스코프의 new.target을 참조하며, 이는 렉시컬 this와 동일한 메커니즘입니다.

bind, call, apply 무효화

입문

화살표 함수의 this는 절대 바꿀 수 없어요! bind, call, apply 같은 특별한 명령어를 써도 소용없어요. 마치 이름이 새겨진 도장처럼 한번 정해지면 영원히 그대로예요!

🎨 bind, call, apply가 뭔가요? 이것들은 일반 함수의 this를 바꿀 수 있는 ‘마법 도구’예요. 마치 그림을 그릴 때 붓의 색을 바꾸는 것처럼, 함수가 가리키는 대상을 바꿀 수 있어요.

🔒 화살표 함수는 왜 안 바뀌나요? 화살표 함수의 this는 태어날 때 이미 ‘각인’되어 있어요. 마치 새끼 오리가 처음 본 엄마를 평생 엄마로 기억하는 것처럼, 화살표 함수도 처음 정해진 this를 절대 잊지 않아요!

💪 bind를 써도 정말 안 되나요? 네, 정말 안 돼요! bind를 쓰면 새로운 함수가 만들어지기는 하지만, this는 여전히 원래 그대로예요. 마치 복사한 사진에 다른 액자를 씌워도 사진 속 사람은 그대로인 것과 같아요.

✨ 이게 좋은 점이 뭔가요? 예측 가능하다는 게 정말 큰 장점이에요! 어떤 상황에서든 this가 바뀌지 않으니까, 코드를 읽을 때 헷갈리지 않아요. 특히 콜백 함수로 쓸 때 실수가 줄어들어요.

⚠️ 주의할 점이 있나요? 라이브러리나 프레임워크에서 this를 특별하게 설정해주는 경우가 있어요. 이럴 때 화살표 함수를 쓰면 그 특별한 this를 받을 수 없어요. 그래서 상황에 맞게 일반 함수와 화살표 함수를 골라 써야 해요!

중급

화살표 함수는 bind(), call(), apply() 메서드로 this를 동적으로 변경할 수 없습니다. 이 메서드들을 호출할 수는 있지만, this 바인딩은 무시되고 렉시컬 this가 유지됩니다.

일반 함수의 this 변경 메서드

  • call(thisArg, …args): 즉시 호출하며 this 설정
  • apply(thisArg, argsArray): 배열로 인수 전달하며 this 설정
  • bind(thisArg, …args): 새 함수를 반환하며 this 고정
function regularFunc() {
  console.log(this.value);
}

const obj1 = { value: 1 };
const obj2 = { value: 2 };

regularFunc.call(obj1); // 1
regularFunc.call(obj2); // 2

const bound = regularFunc.bind(obj1);
bound.call(obj2); // 1 (bind가 우선)
const obj1 = { value: 1 };
const obj2 = { value: 2 };

const arrowFunc = () => {
  console.log(this.value);
};

arrowFunc.call(obj1); // undefined (전역 this 유지)
arrowFunc.apply(obj2); // undefined (전역 this 유지)

const bound = arrowFunc.bind(obj1);
bound(); // undefined (렉시컬 this 유지)
class Button {
  constructor() {
    this.count = 0;
  }

  // 일반 함수: this를 바꿔야 함
  handleClickRegular(event) {
    this.count++; // this는 button 엘리먼트
  }

  // 화살표 함수: this가 자동으로 유지됨
  handleClickArrow = (event) => {
    this.count++; // this는 Button 인스턴스
  }
}

const btn = new Button();
// DOM API가 this를 변경해도 화살표 함수는 영향 없음
element.addEventListener('click', btn.handleClickArrow);

메서드 호출 동작 bind, call, apply를 화살표 함수에 호출하면:

  • thisArg 인수는 무시됨
  • 나머지 인수는 정상적으로 전달됨
  • 에러는 발생하지 않음 (조용히 무시)

심화

화살표 함수의 bind/call/apply 무효화는 ECMAScript 명세에서 [[Call]] 내부 메서드 구현 차이로 나타납니다.

ECMAScript 명세 기반 ThisBinding 메커니즘 ECMAScript 2023, Section 10.2.1.1 (PrepareForOrdinaryCall)에서 this 바인딩 과정을 정의합니다.

일반 함수의 [[Call]](thisArgument, argumentsList):

  1. callerContext = running execution context
  2. calleeContext = PrepareForOrdinaryCall(F, undefined)
  3. OrdinaryCallBindThis(F, calleeContext, thisArgument):
    • thisMode = F.[[ThisMode]]
    • If thisMode is “lexical”, return NormalCompletion(undefined)
    • If thisMode is “strict”, thisValue = thisArgument
    • Else, thisValue = ToObject(thisArgument) or globalThis
    • calleeContext.ThisBinding = thisValue
  4. OrdinaryCallEvaluateBody(F, argumentsList)

화살표 함수의 [[Call]](thisArgument, argumentsList):

  1. PrepareForOrdinaryCall 동일
  2. OrdinaryCallBindThis에서 [[ThisMode]] = “lexical” 확인
  3. ThisBinding 설정 스킵 (즉시 return)
  4. GetThisValue 시 외부 환경 레코드 탐색

Function.prototype.bind 구현 Section 20.2.3.2 (Function.prototype.bind):

  1. BoundFunctionCreate(targetFunction, boundThis, boundArgs)
  2. [[BoundTargetFunction]] = targetFunction
  3. [[BoundThis]] = boundThis
  4. [[Call]] 호출 시 targetFunction.[[Call]](boundThis, args)

화살표 함수가 target이면:

  • boundThis는 전달되지만 무시됨
  • [[ThisMode]] = “lexical”이므로 OrdinaryCallBindThis가 no-op

V8 엔진 구현 상세 V8에서 bind/call/apply는 다음과 같이 최적화됩니다:

Fast Path for Arrow Functions: V8은 화살표 함수의 [[ThisMode]]를 검사하여 thisArg 처리를 건너뜁니다:

if (function->shared()->is_arrow_function()) {
  // Skip thisArg processing
  return InvokeFunction(function, undefined, args);
}

Bound Function Optimization: 바운드 함수는 별도의 JSBoundFunction 객체로 생성되며, 호출 시 thisArg를 언박싱합니다. 화살표 함수가 target이면 이 과정이 낭비되므로, TurboFan은 바운드 화살표 함수를 인라인할 때 thisArg 전달을 제거합니다.

성능 특성 call/apply 호출 비용:

  • 일반 함수: thisArg 변환 + 바인딩 (평균 10-20ns)
  • 화살표 함수: thisArg 무시 (평균 5ns)

bind 호출 비용:

  • 일반 함수: BoundFunction 객체 생성 (평균 50ns)
  • 화살표 함수: 동일 (객체는 생성되지만 무의미)

실무적 함의 라이브러리 API 설계 시 주의사항:

  • thisArg를 받는 API(예: Array.prototype.forEach(callback, thisArg))는 화살표 함수와 호환되지 않음을 문서화해야 합니다.
  • 콜백 함수에 this 컨텍스트를 제공하는 패턴은 화살표 함수 사용을 방해하므로, 파라미터로 전달하는 방식을 선호해야 합니다.