바인딩 우선순위 규칙은?

new, 명시적, 암시적, 기본 바인딩의 우선순위 체계를 이해하고 복잡한 상황에서 this가 어떻게 결정되는지 예측하는 방법을 마스터합니다

심화 15분 우선순위 바인딩 규칙 new 명시적 바인딩

JavaScript에서 this가 무엇을 가리키는지 결정하는 규칙은 하나가 아닙니다. 기본 바인딩, 암시적 바인딩, 명시적 바인딩, 그리고 new 바인딩까지 네 가지 규칙이 존재하며, 이들이 동시에 적용될 수 있는 상황에서는 반드시 우선순위를 알아야 올바른 결과를 예측할 수 있습니다. 각각의 바인딩 규칙을 개별적으로 이해하는 것만으로는 충분하지 않으며, 여러 규칙이 충돌하는 복합적인 상황에서 어떤 규칙이 최종적으로 적용되는지를 판단하는 체계적인 사고 방식이 필요합니다. 바인딩 우선순위는 this에 대한 개별 지식을 통합하여 실전에서 활용할 수 있게 만드는 핵심 개념입니다.

핵심 특징

  • 🔢 네 가지 바인딩 규칙 간에 명확한 우선순위 체계가 존재한다new 바인딩이 가장 높고, 기본 바인딩이 가장 낮은 계층 구조를 형성합니다
  • ⚖️ 명시적 바인딩은 암시적 바인딩보다 항상 우선한다call, apply, bind로 지정한 this는 객체 메서드 호출 방식보다 강하게 적용됩니다
  • 🏗️ new 바인딩은 bind로 고정한 this마저 덮어쓸 수 있다 — 생성자 호출은 모든 바인딩 규칙 중 가장 높은 우선순위를 가집니다
  • 📋 우선순위 판단은 위에서 아래로 순차적으로 확인하는 체크리스트 방식이 가장 효과적이다 — 가장 높은 우선순위부터 해당 여부를 확인하면 복잡한 상황도 체계적으로 해결할 수 있습니다

실무에서의 영향

실무 코드에서는 하나의 함수가 다양한 컨텍스트에서 호출되는 경우가 빈번합니다. 이벤트 핸들러로 등록된 메서드가 bindthis를 고정한 상태에서 다시 객체의 프로퍼티로 할당되거나, 라이브러리 내부에서 call이나 apply로 호출되는 복합적인 상황이 자주 발생합니다. 바인딩 우선순위를 정확히 이해하지 못하면 이런 상황에서 this가 예상과 다른 값을 가리키는 버그를 만들게 되고, 디버깅에 상당한 시간을 소비하게 됩니다. 특히 React 클래스 컴포넌트의 메서드 바인딩, 프레임워크의 의존성 주입, 콜백 패턴 등에서 우선순위 지식은 코드의 동작을 정확히 예측하는 데 직접적인 영향을 줍니다. 우선순위 체계를 체크리스트로 내재화하면 복잡한 호출 패턴을 마주해도 this의 값을 즉시 판단할 수 있어 코드 리뷰와 디버깅 효율이 크게 향상됩니다. 이 지식은 this 바인딩에 대한 개별 학습을 실전 역량으로 전환하는 마지막 퍼즐 조각입니다.


핵심 개념

네 가지 바인딩 규칙

입문

JavaScript에서 this가 가리키는 대상을 정하는 규칙은 총 네 가지가 있어요. 각 규칙이 어떤 상황에서 적용되는지 알아볼까요!

📋 네 가지 규칙이 뭔가요? 학교에서 반장을 뽑는 방법이 여러 가지인 것처럼, this가 누구를 가리킬지 정하는 방법도 네 가지예요. 기본 바인딩, 암시적 바인딩, 명시적 바인딩, new 바인딩이 바로 그 네 가지랍니다.

🏠 기본 바인딩은 뭔가요? 아무런 특별한 규칙이 없을 때 적용되는 가장 기본적인 규칙이에요. 마치 아무도 반장 후보를 내지 않으면 선생님이 알아서 정해주는 것처럼, 함수를 그냥 단독으로 호출하면 this는 기본값(전역 객체나 undefined)을 가리켜요.

📦 암시적 바인딩은 뭔가요? 어떤 물건의 주인이 자연스럽게 정해지는 것과 비슷해요. 리모컨을 TV에 꽂아서 사용하면 리모컨은 자연스럽게 그 TV를 조작하죠? 함수를 어떤 객체의 소속으로 호출하면, this는 자연스럽게 그 객체를 가리켜요.

🎯 명시적 바인딩은 뭔가요? “너는 이 사람의 말을 들어!”라고 직접 지정해주는 거예요. 학교에서 선생님이 “오늘은 A반 학생도 B반 선생님 말을 들어”라고 직접 정해주는 것처럼, 특별한 명령어로 this가 가리킬 대상을 강제로 지정할 수 있어요.

🏗️ new 바인딩은 뭔가요? 새로운 물건을 만드는 공장과 같아요. 공장에서 새 로봇을 만들면, 그 로봇의 리모컨은 항상 새로 만든 로봇을 조종하게 되죠? new라는 명령어로 새로운 객체를 만들면, this는 항상 그 새로 만들어진 객체를 가리켜요.

중급

JavaScript에서 this가 결정되는 네 가지 바인딩 규칙은 각각 다른 호출 패턴에 의해 활성화됩니다.

기본 바인딩 (Default Binding) 함수를 단독 호출할 때 적용됩니다. strict mode에서는 undefined, 비엄격 모드에서는 전역 객체(window 또는 globalThis)를 가리킵니다.

암시적 바인딩 (Implicit Binding) 함수를 객체의 메서드로 호출할 때 적용됩니다. 호출 시점에 점(.) 앞에 있는 객체가 this가 됩니다.

명시적 바인딩 (Explicit Binding) call, apply, bind 메서드를 사용하여 this를 직접 지정합니다.

new 바인딩 (New Binding) new 키워드로 함수를 생성자로 호출하면, 새로 생성된 객체가 this가 됩니다.

function greet() {
  console.log(this?.name);
}

const obj = { name: 'Kim', greet };

// 1. 기본 바인딩 - 단독 호출
greet(); // undefined (strict mode)

// 2. 암시적 바인딩 - 객체 메서드 호출
obj.greet(); // 'Kim'

// 3. 명시적 바인딩 - call로 this 지정
greet.call({ name: 'Lee' }); // 'Lee'

// 4. new 바인딩 - 생성자 호출
function User(name) {
  this.name = name;
}
const user = new User('Park'); // this → 새 객체 { name: 'Park' }

심화

네 가지 바인딩 규칙은 ECMAScript 명세의 함수 호출 메커니즘에서 각각 다른 추상 연산(Abstract Operation)을 통해 this 값을 결정합니다.

ECMAScript 명세의 this 결정 메커니즘 ECMAScript 명세 Section 10.2.1.1 OrdinaryCallBindThis에 따르면, 함수 호출 시 this 값은 thisArgument 매개변수로 전달됩니다. 이 값이 어떻게 결정되는지는 호출 방식에 따라 달라집니다.

기본 바인딩에서는 Section 7.3.18 EvaluateCall이 thisValue로 undefined를 전달합니다. 이후 OrdinaryCallBindThis에서 함수의 내부 슬롯 [[ThisMode]]를 확인하여, strict이면 undefined를 그대로 사용하고, global이면 전역 객체로 대체합니다.

암시적 바인딩에서는 MemberExpression 평가 시 Reference Record(참조 레코드)가 생성되며, 이 레코드의 [[Base]] 값이 thisValue로 사용됩니다. 즉 obj.method()에서 Reference Record의 [[Base]]는 obj가 됩니다.

명시적 바인딩과 new 바인딩의 내부 동작 명시적 바인딩은 Section 20.2.3.3 Function.prototype.call에 정의된 대로, 첫 번째 인수를 thisArg로 받아 [[Call]] 내부 메서드에 직접 전달합니다. bind는 Section 20.2.3.2에서 BoundFunctionCreate를 호출하여 [[BoundThis]] 내부 슬롯에 this 값을 저장하는 exotic object(특수 객체)를 생성합니다.

new 바인딩은 Section 13.3.5 EvaluateNew에서 Construct 추상 연산을 호출합니다. 이때 OrdinaryCreateFromConstructor로 새 객체를 생성한 뒤, 이 객체를 thisArgument로 전달합니다. 이 과정은 함수의 [[Construct]] 내부 메서드에서 수행되며, [[Call]]과는 완전히 다른 실행 경로를 따릅니다.

암시적 바인딩 vs 명시적 바인딩

입문

같은 함수를 호출할 때, 객체에 소속되어 호출하는 방법과 직접 대상을 지정하는 방법이 충돌하면 어떻게 될까요?

🏫 반장 선출 비유로 알아봐요 교실에서 반장을 뽑는데, A반 학생들이 투표로 민수를 뽑았어요(암시적 바인딩). 그런데 교장 선생님이 “이번 반장은 영희야”라고 직접 지정했어요(명시적 바인딩). 이럴 때 누가 반장이 될까요? 교장 선생님의 결정이 더 강하니까 영희가 반장이 되겠죠!

💪 왜 직접 지정이 더 강한가요? 자연스럽게 정해진 것보다 의도적으로 정한 것이 더 강한 건 당연해요. “네가 알아서 해”보다 “이렇게 해!”가 더 확실한 명령인 것처럼, 명시적으로 지정한 this가 자연스럽게 정해진 this보다 항상 우선해요.

🔄 실제로는 어떤 상황인가요? 친구네 집 리모컨을 빌려왔는데, 우리 집 TV에 연결해서 쓰고 싶은 상황이에요. 리모컨은 원래 친구네 TV(암시적 바인딩)에 연결되어 있지만, 우리가 직접 우리 집 TV로 연결을 바꿀 수(명시적 바인딩) 있어요. 직접 바꾼 연결이 원래 연결보다 우선하는 거예요!

📌 기억할 포인트 정리하면, 자연스러운 소속(암시적)보다 강제로 지정(명시적)한 것이 항상 이겨요. 이건 바인딩 우선순위에서 가장 기본이 되는 규칙이에요.

중급

명시적 바인딩은 항상 암시적 바인딩보다 우선합니다. 객체의 메서드로 호출하더라도, call, apply, bindthis를 지정하면 그 값이 사용됩니다.

암시적 바인딩이 무시되는 과정 obj.method.call(other)처럼 호출하면, methodobj에 소속되어 있더라도 thisother를 가리킵니다. call이 호출 시점에 this를 강제로 교체하기 때문입니다.

const obj1 = {
  name: 'obj1',
  greet() {
    console.log(this.name);
  }
};

const obj2 = { name: 'obj2' };

// 암시적 바인딩: obj1이 this
obj1.greet(); // 'obj1'

// 명시적 바인딩이 암시적 바인딩을 덮어씀
obj1.greet.call(obj2); // 'obj2'

bind로 고정한 this는 이후 암시적 바인딩을 무시한다 bindthis가 영구적으로 고정된 새 함수를 반환합니다. 이 함수를 어떤 객체의 메서드로 할당하더라도, thisbind로 고정한 값을 유지합니다.

function greet() {
  console.log(this.name);
}

const fixed = { name: 'Fixed' };
const boundGreet = greet.bind(fixed);

const obj = {
  name: 'Obj',
  greet: boundGreet
};

// 암시적 바인딩(obj)이 아닌 bind로 고정한 값(Fixed)이 사용됨
obj.greet(); // 'Fixed'

심화

명시적 바인딩이 암시적 바인딩보다 우선하는 이유는 ECMAScript 명세의 함수 호출 경로에서 thisArgument가 결정되는 시점의 차이에 기인합니다.

Reference Record와 명시적 thisArg의 충돌 해소 암시적 바인딩에서는 Section 13.3.8.1 EvaluateCall이 Reference Record의 [[Base]]를 thisValue로 추출합니다. 그러나 call이나 apply가 호출되면, 이 과정 자체가 우회됩니다. Function.prototype.call(Section 20.2.3.3)은 자체적으로 [[Call]] 내부 메서드를 호출하면서 첫 번째 인수를 thisArg로 직접 전달하므로, Reference Record에서 추출한 thisValue는 사용되지 않습니다.

핵심은 obj.method.call(other)에서 call 자체가 obj를 통해 암시적으로 호출되지만, call의 구현이 내부적으로 method의 [[Call]]을 other를 thisArg로 하여 호출한다는 점입니다. 즉 call에 대한 암시적 바인딩(obj)은 call 함수 내부에서 소비되고, 원래 함수(method)에는 명시적으로 전달된 other가 thisArg로 도달합니다.

BoundFunctionExoticObject의 this 고정 메커니즘 bind가 생성하는 Bound Function Exotic Object(바운드 함수 특수 객체)는 [[BoundThis]] 내부 슬롯에 this 값을 저장합니다. Section 10.4.1.1 [[Call]]에 따르면, 바운드 함수가 호출될 때 전달된 thisArgument는 완전히 무시되고 [[BoundThis]]가 대신 사용됩니다. 이는 암시적 바인딩뿐 아니라 call이나 apply로 전달한 thisArg도 무시한다는 것을 의미합니다. 바운드 함수에 다시 call을 적용해도 [[BoundThis]]는 변경되지 않습니다.

new 바인딩의 최고 우선순위

입문

new 바인딩은 모든 규칙 중에서 가장 강력해요. 심지어 명시적으로 고정한 this도 무시할 수 있답니다!

👑 왕의 명령 비유 교장 선생님(명시적 바인딩)이 “반장은 영희야”라고 정했어도, 교육부 장관(new 바인딩)이 “새 학교를 만들고 거기서 새 반장을 뽑아”라고 하면 완전히 새로운 결과가 나와요. new는 아예 새로운 것을 만들기 때문에 기존의 모든 지정을 무시할 수 있어요.

🏭 새 공장 비유 리모컨을 특정 TV에 영구적으로 연결해놨어도(bind), 아예 새로운 TV를 만드는 공장(new)이 가동되면 새 TV 전용 리모컨이 만들어져요. 기존에 어디에 연결되어 있었든 상관없이, 새로 만든 물건은 항상 새 연결을 가져요.

❓ 왜 new가 가장 강한가요? new는 아예 “새로운 세계”를 만드는 명령이에요. 기존 규칙들은 이미 있는 대상 중에서 누구를 가리킬지 정하는 것이지만, new는 새로운 대상 자체를 만들어버려요. 존재하지 않던 것을 만드는 행위이니 기존 규칙보다 강할 수밖에 없어요.

💡 정리하면 new 바인딩 > 명시적 바인딩 > 암시적 바인딩 > 기본 바인딩 순서로 강해요. new가 가장 강하고, 기본이 가장 약해요. 이 순서만 기억하면 어떤 상황에서도 this를 예측할 수 있어요!

중급

new 바인딩은 가장 높은 우선순위를 가집니다. 심지어 bindthis를 고정한 함수에 new를 사용하면, bind로 고정한 this가 무시되고 새로 생성된 객체가 this가 됩니다.

new가 bind를 이기는 이유 new는 함수를 생성자로 호출하면서 완전히 새로운 객체를 만들어 this에 바인딩합니다. 이 과정은 bind로 고정된 this와 무관하게 동작합니다. bind가 인수 부분 적용(partial application)을 위해 사용되면서도 new로 생성자 호출이 가능해야 하기 때문에, 명세에서 의도적으로 이런 우선순위를 설정했습니다.

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

const obj = { name: 'Bound' };
const BoundUser = User.bind(obj);

// bind로 this를 obj에 고정했지만...
BoundUser('Lee');
console.log(obj.name); // 'Lee' - bind가 적용됨

// new는 bind를 이긴다
const user = new BoundUser('Park');
console.log(user.name); // 'Park' - 새 객체가 this
console.log(obj.name);  // 'Lee' - obj는 영향받지 않음

주의: call/apply는 new와 함께 사용할 수 없다 newcall/apply는 직접적으로 충돌하지 않습니다. new는 생성자 호출, call/apply는 일반 호출이기 때문에 같은 호출에서 동시에 적용할 수 없습니다. 따라서 “new vs call/apply”는 우선순위 비교 대상이 아닙니다. new가 이기는 대상은 bind로 고정된 this입니다.

심화

new 바인딩이 bind를 이기는 메커니즘은 ECMAScript 명세에서 Bound Function Exotic Object의 [[Construct]] 내부 메서드 동작에 명시적으로 정의되어 있습니다.

[[Construct]]와 [[Call]]의 분기 동작 Section 10.4.1.2 [[Construct]]에 따르면, 바운드 함수가 new로 호출될 때의 동작은 [[Call]]과 근본적으로 다릅니다. [[Call]]에서는 [[BoundThis]]를 thisArgument로 사용하지만, [[Construct]]에서는 [[BoundThis]]를 완전히 무시합니다. 대신 target 함수(원본 함수)의 [[Construct]]를 호출하면서, new가 생성한 새 객체를 thisArgument로 전달합니다.

의사 코드로 표현하면:

  • boundFn() → target.[[Call]](boundThis, args) — BoundThis 사용
  • new boundFn() → target.[[Construct]](args, newTarget) — BoundThis 무시, 새 객체 사용

이 설계는 의도적입니다. bind의 주요 용도 중 하나인 부분 적용(Partial Application)과 new를 함께 사용할 수 있도록 보장합니다. bind로 인수를 미리 채우되, new로 호출 시에는 새 인스턴스를 정상적으로 생성할 수 있어야 하기 때문입니다.

Reflect.construct와 new.target의 역할 ES2015에서 도입된 Reflect.construct(Section 26.1.2)는 new의 동작을 프로그래밍적으로 제어할 수 있게 합니다. new.target(Section 13.3.5)은 [[Construct]]로 호출되었는지를 함수 내부에서 확인하는 메타 프로퍼티(Meta Property)입니다. 이 메커니즘을 통해 함수가 new로 호출되었는지 여부에 따라 다른 동작을 구현할 수 있으며, new로 호출되었을 때 this는 항상 새로 생성된 객체가 됩니다.

바인딩 판별 체크리스트

입문

this가 무엇을 가리키는지 헷갈릴 때, 위에서부터 순서대로 확인하면 항상 정답을 찾을 수 있는 체크리스트가 있어요!

📝 체크리스트가 뭔가요? 비행기 조종사가 이륙 전에 확인 목록을 하나씩 체크하는 것처럼, this를 판별할 때도 정해진 순서대로 하나씩 확인하는 방법이에요. 위에서부터 확인하면서 처음 해당하는 규칙이 바로 정답이에요.

1️⃣ 첫 번째 확인: new로 호출했나요? 가장 먼저 확인할 것은 “new 키워드를 썼나?”예요. 만약 new를 썼다면 더 볼 것도 없이 this는 새로 만든 객체예요. 가장 강력한 규칙이니까 가장 먼저 확인해요.

2️⃣ 두 번째 확인: call, apply, bind를 썼나요? new를 안 썼다면 다음으로 “직접 this를 지정했나?”를 확인해요. call, apply, bind 같은 방법으로 this를 지정했다면, 그 지정한 대상이 this예요.

3️⃣ 세 번째 확인: 객체의 메서드로 호출했나요? 명시적 지정도 없다면 “점(.) 앞에 객체가 있나?”를 확인해요. obj.method()처럼 호출했다면 점 앞의 obj가 this예요.

4️⃣ 마지막: 아무것도 해당 안 되면? 위 세 가지 모두 해당하지 않으면 기본 규칙이 적용돼요. 엄격 모드에서는 undefined, 그렇지 않으면 전역 객체가 this가 되어요.

중급

this 바인딩 판별은 우선순위가 높은 규칙부터 순서대로 확인하는 체크리스트 방식이 가장 효과적입니다.

4단계 판별 체크리스트

  1. new 바인딩 확인: new로 호출되었는가? → this는 새로 생성된 객체
  2. 명시적 바인딩 확인: call, apply, bind가 사용되었는가? → this는 지정된 객체
  3. 암시적 바인딩 확인: 객체의 메서드로 호출되었는가? → this는 호출 객체
  4. 기본 바인딩 적용: 위 어느 것도 해당하지 않으면 → strict mode: undefined / 비엄격 모드: 전역 객체

첫 번째로 해당하는 규칙이 최종 결과입니다. 이후 규칙은 확인할 필요가 없습니다.

function identify() {
  console.log(this?.name);
}

const obj = { name: 'A', identify };
const bound = identify.bind({ name: 'B' });

// Case 1: new 바인딩 → 체크리스트 1번 해당
new identify();        // this → 새 객체 (name: undefined)

// Case 2: 명시적 바인딩 → 체크리스트 2번 해당
identify.call({ name: 'C' }); // 'C'

// Case 3: 암시적 바인딩 → 체크리스트 3번 해당
obj.identify();        // 'A'

// Case 4: 기본 바인딩 → 체크리스트 4번 해당
identify();            // undefined (strict mode)

복합 상황에서의 적용 여러 규칙이 겹치는 복합 상황에서도 체크리스트를 위에서 아래로 적용하면 됩니다. new bound()는 체크리스트 1번(new)에 해당하므로, bind로 고정한 this는 무시됩니다.

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

const obj = { name: 'Fixed' };
const BoundUser = User.bind(obj);

// bind + new가 동시에 존재
// 체크리스트: 1번(new) 해당 → 새 객체가 this
const user = new BoundUser('Kim');
console.log(user.name); // 'Kim' (새 객체)
console.log(obj.name);  // 'Fixed' (변경 없음)

심화

바인딩 판별 체크리스트는 단순한 학습 도구가 아니라, ECMAScript 명세의 함수 호출 메커니즘이 실제로 this를 결정하는 순서를 반영한 것입니다.

명세 기반 판별 알고리즘 ECMAScript 명세에서 함수 호출은 [[Call]]과 [[Construct]] 두 가지 내부 메서드로 나뉩니다. new 키워드가 있으면 [[Construct]]가 호출되고, 없으면 [[Call]]이 호출됩니다. 이 분기가 체크리스트의 첫 번째 단계(new 바인딩 확인)에 해당합니다.

[[Call]] 경로에서는 OrdinaryCallBindThis(Section 10.2.1.2)가 thisArgument를 받아 thisValue를 결정합니다. 이때 thisArgument는 호출 방식에 따라 다르게 전달됩니다:

  • call/apply → 사용자가 전달한 첫 번째 인수 (명시적 바인딩)
  • 바운드 함수 → [[BoundThis]] 내부 슬롯 (명시적 바인딩)
  • Reference Record → [[Base]] 값 (암시적 바인딩)
  • 그 외 → undefined (기본 바인딩)

arrow function과 체크리스트의 관계 화살표 함수(Arrow Function)는 이 체크리스트의 예외입니다. 화살표 함수는 [[ThisMode]]가 lexical로 설정되어 있어, OrdinaryCallBindThis에서 thisArgument를 아예 무시합니다. 대신 렉시컬 환경(Lexical Environment)의 외부 스코프에서 this를 상속받습니다. 따라서 체크리스트 적용 전에 “화살표 함수인가?”를 먼저 확인해야 합니다. 화살표 함수라면 네 가지 규칙 모두 적용되지 않으며, 정의 시점의 외부 this가 그대로 사용됩니다.

Symbol.hasInstance와 @@construct의 확장 ES2015 이후 Symbol.hasInstance를 통해 instanceof 동작을 커스터마이징할 수 있으며, Proxy의 construct 트랩(trap)을 통해 [[Construct]] 동작 자체를 가로챌 수 있습니다. 이는 체크리스트의 1번 단계(new 바인딩)에서 this가 결정되는 방식을 프로그래밍적으로 변경할 수 있음을 의미합니다. 하지만 일반적인 함수 호출에서는 명세에 정의된 기본 동작이 적용됩니다.

예외 상황과 특수 케이스

입문

바인딩 우선순위 규칙에는 몇 가지 특별한 예외가 있어요. 규칙대로 하면 안 되는 특수한 경우를 알아볼까요!

🏹 화살표 함수는 특별해요 화살표 함수는 모든 규칙을 무시하는 특별한 존재예요. 마치 “나는 태어난 곳의 규칙만 따를 거야”라고 선언한 학생 같아요. 어떤 방법을 써도 화살표 함수의 this는 바꿀 수 없어요. 태어난 곳(만들어진 위치)의 this를 평생 사용해요.

🚫 null이나 undefined를 넘기면요? 명시적 바인딩에서 this로 null이나 undefined를 지정하면 어떻게 될까요? 마치 “반장을 아무도 안 시킬 거야”라고 한 것과 같아요. 이러면 명시적 바인딩이 무시되고 기본 바인딩이 대신 적용돼요.

🔗 bind를 여러 번 하면요? 같은 함수에 bind를 두 번, 세 번 하면 어떻게 될까요? 처음 bind한 것만 유효해요! 마치 편지에 처음 쓴 주소로만 배달되고, 나중에 덧쓴 주소는 무시되는 것과 같아요.

⚡ setTimeout에서는요? setTimeout 같은 타이머 함수에 메서드를 전달하면, 원래 객체와의 연결이 끊어져요. 마치 택배 기사에게 물건을 맡기면, 물건이 원래 가게가 아닌 택배 기사의 손에 있는 것처럼요. 이럴 때 bind나 화살표 함수로 this를 보호해야 해요.

중급

기본적인 네 가지 바인딩 규칙 외에, 실무에서 자주 마주치는 예외 상황들이 있습니다. 이 케이스들을 알아야 실전에서 정확한 판단이 가능합니다.

화살표 함수: 체크리스트 밖의 존재 화살표 함수는 자체 this를 가지지 않으며, call, apply, bind, new 모두 this를 변경할 수 없습니다. 렉시컬 스코프(정의된 위치)의 this를 그대로 사용합니다.

const obj = {
  name: 'obj',
  arrow: () => {
    console.log(this.name); // 외부(전역)의 this
  },
  regular() {
    const inner = () => {
      console.log(this.name); // regular의 this를 상속
    };
    inner();
  }
};

obj.arrow();                    // undefined (전역 this)
obj.arrow.call({ name: 'X' }); // undefined (call 무시)
obj.regular();                  // 'obj' (암시적 바인딩 상속)

null/undefined 전달 시 기본 바인딩 폴백 call, apply, bindnull이나 undefined를 전달하면, 비엄격 모드에서는 기본 바인딩(전역 객체)으로 폴백됩니다. 이는 의도치 않은 전역 오염의 원인이 됩니다.

function log() {
  console.log(this);
}

// null 전달 → 비엄격 모드에서 전역 객체로 폴백
log.call(null); // window (비엄격) / null (strict)

// 안전한 빈 this 패턴: Object.create(null)
const empty = Object.create(null);
log.call(empty); // {} - 프로토타입 없는 빈 객체

bind의 중첩은 첫 번째만 유효 bind를 여러 번 호출해도 최초 bindthis만 적용됩니다. 이후 bind 호출은 this를 변경하지 못하고, 인수 부분 적용만 누적됩니다.

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const first = greet.bind({ name: 'First' });
const second = first.bind({ name: 'Second' });

// 두 번째 bind의 this({ name: 'Second' })는 무시됨
second('Hi', '!'); // 'Hi, First!'

심화

예외 상황들은 ECMAScript 명세의 세부 동작에서 비롯되며, 각각 명확한 명세적 근거를 가지고 있습니다.

화살표 함수의 [[ThisMode]]: lexical Section 10.2.1 OrdinaryCallBindThis에서, 함수의 [[ThisMode]] 내부 슬롯이 lexical이면 this 바인딩 과정 전체를 건너뜁니다(early return). 화살표 함수는 Section 15.3에서 정의되며, MakeMethod나 SetFunctionName 과정에서 [[ThisMode]]를 lexical로 설정합니다. 이로 인해 call, apply, bind의 thisArg가 전달되더라도 함수 환경 레코드(Function Environment Record)에 this 바인딩이 생성되지 않습니다. 대신 GetThisEnvironment(Section 9.4.3)가 외부 환경 레코드를 탐색하여 가장 가까운 non-lexical 환경의 this를 반환합니다.

또한 화살표 함수는 [[Construct]] 내부 메서드를 가지지 않으므로, new와 함께 사용하면 TypeError가 발생합니다. 이는 IsConstructor(Section 7.2.4) 검사에서 [[Construct]]의 존재 여부를 확인하기 때문입니다.

null/undefined thisArg의 처리 OrdinaryCallBindThis에서 [[ThisMode]]가 strict이면 thisValue를 그대로 사용합니다(null이든 undefined이든). 그러나 global 모드에서는 thisArgument가 null 또는 undefined일 때 전역 환경 레코드(Global Environment Record)의 [[GlobalThisValue]]로 대체합니다. 이 동작은 ES3부터의 하위 호환성을 위한 것이며, 이를 활용하여 apply(null, args) 패턴으로 인수를 전개(spread)하는 관용구가 존재했습니다. ES2015의 spread 연산자(...args)가 이 패턴을 대체하므로, 현대 코드에서는 null 전달을 피하는 것이 권장됩니다.

bind 중첩의 [[BoundTargetFunction]] 체이닝 bind를 중첩 호출하면 바운드 함수가 다른 바운드 함수를 [[BoundTargetFunction]]으로 참조하는 체인이 형성됩니다. 호출 시 각 바운드 함수의 [[Call]]이 순차적으로 호출되면서, 가장 바깥 바운드 함수의 [[BoundThis]]가 사용되고, [[BoundArgs]]는 순서대로 병합(prepend)됩니다. 결과적으로 가장 먼저 호출된 bind의 this만 최종 함수에 도달하며, 이후 bind의 this는 체인의 중간 단계에서 소비되고 폐기됩니다.