this 바인딩 — 호출 방식이 결정하는 네 가지 규칙
같은 함수인데
obj.greet()로 호출하면this가obj이고,const fn = obj.greet; fn()으로 호출하면this가undefined가 됩니다. 왜 호출 방식에 따라this가 달라지는 걸까요?
자바스크립트의 this는 함수가 선언된 위치가 아니라 호출된 방식 에 의해 결정됩니다. 이 특성 때문에 콜백으로 메서드를 전달할 때 this가 풀리는 문제가 발생합니다.
this는 호출 시점에 결정된다
자바스크립트의 this는 함수가 선언된 위치가 아니라, 호출된 방식 에 의해 결정됩니다. 이것이 다른 언어와 가장 큰 차이점입니다.
네 가지 바인딩 규칙이 있고, 우선순위도 정해져 있습니다.
규칙 1: 기본 바인딩 (Default Binding)
아무 조건에도 해당하지 않을 때 적용되는 기본 규칙입니다.
function showThis() {
console.log(this);
}
showThis(); // 비엄격 모드: window (전역 객체) / 엄격 모드: undefined
엄격 모드의 영향
'use strict';
function strictShow() {
console.log(this);
}
strictShow(); // undefined
비엄격 모드에서 this가 전역 객체로 바인딩되는 것은 언어 설계상의 실수로 여겨집니다. 엄격 모드에서는 이 동작이 수정되어 undefined가 됩니다.
ES 모듈(type="module")은 기본적으로 엄격 모드이므로, 현대 프로젝트에서는 대부분 undefined입니다.
규칙 2: 암묵적 바인딩 (Implicit Binding)
함수가 객체의 메서드로 호출 될 때, this는 그 객체를 가리킵니다.
const user = {
name: 'Alice',
greet() {
console.log(`안녕하세요, ${this.name}입니다.`);
}
};
user.greet(); // '안녕하세요, Alice입니다.'
**핵심 **: 호출 시점에 . 앞에 있는 객체가 this입니다.
암묵적 바인딩의 함정 — 참조 손실
const user = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
// 메서드를 변수에 할당하면 참조가 끊어짐
const fn = user.greet;
fn(); // undefined (비엄격 모드에서는 window.name)
fn()은 단순 함수 호출이므로 기본 바인딩이 적용됩니다. 이 패턴은 ** 콜백으로 메서드를 전달할 때** 자주 발생합니다.
const user = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
// 콜백으로 전달하면 암묵적 바인딩이 사라짐
setTimeout(user.greet, 1000); // undefined
중첩 객체
중첩 객체에서는 ** 직전 객체 **가 this입니다.
const company = {
name: '회사',
team: {
name: '개발팀',
getName() {
return this.name;
}
}
};
console.log(company.team.getName()); // '개발팀' — team이 this
규칙 3: 명시적 바인딩 (Explicit Binding)
call, apply, bind를 사용하여 this를 직접 지정합니다.
call
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const user = { name: 'Alice' };
greet.call(user, '안녕하세요'); // '안녕하세요, Alice'
인자를 ** 개별적으로** 전달합니다.
apply
greet.apply(user, ['안녕하세요']); // '안녕하세요, Alice'
인자를 ** 배열로** 전달합니다. call과 apply의 차이는 인자 전달 방식뿐입니다.
// 외우는 팁: Apply는 Array, Call은 Comma
bind
const boundGreet = greet.bind(user);
boundGreet('안녕하세요'); // '안녕하세요, Alice'
bind는 ** 새로운 함수를 반환 **합니다. call/apply처럼 즉시 실행하지 않고, this가 고정된 함수를 만듭니다.
// 콜백에서의 this 문제 해결
const user = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
// bind로 this 고정
setTimeout(user.greet.bind(user), 1000); // 'Alice'
하드 바인딩
bind로 고정된 this는 다시 변경할 수 없습니다.
function show() {
console.log(this.name);
}
const bound = show.bind({ name: 'Alice' });
bound.call({ name: 'Bob' }); // 'Alice' — call로도 변경 불가
규칙 4: new 바인딩
new 키워드로 함수를 호출하면 새로운 객체가 생성되고, this가 그 객체에 바인딩됩니다.
function User(name) {
// this = {} (새로 생성된 빈 객체)
this.name = name;
// return this (암묵적 반환)
}
const user = new User('Alice');
console.log(user.name); // 'Alice'
new가 호출되면 내부적으로 네 가지 일이 벌어집니다.
- 빈 객체 생성
- 새 객체의
[[Prototype]]을 생성자의prototype에 연결 this를 새 객체에 바인딩하고 함수 실행- 함수가 객체를 반환하지 않으면 새 객체를 자동 반환
바인딩 우선순위
네 가지 규칙이 충돌할 때 ** 우선순위 **가 있습니다.
new 바인딩 > 명시적 바인딩(bind) > 암묵적 바인딩 > 기본 바인딩
우선순위 확인
function foo() {
console.log(this.a);
}
const obj1 = { a: 1, foo };
const obj2 = { a: 2, foo };
// 암묵적 vs 명시적 → 명시적 승리
obj1.foo.call(obj2); // 2
// bind vs 암묵적 → bind 승리
const boundFoo = foo.bind(obj1);
obj2.foo = boundFoo;
obj2.foo(); // 1
// new vs bind → new 승리
const BoundFoo = foo.bind(obj1);
const instance = new BoundFoo();
console.log(instance.a); // undefined — 새 객체가 this
화살표 함수의 렉시컬 this
화살표 함수(=>)는 위 네 가지 규칙을 ** 전부 무시 **합니다. 자체 this가 없고, ** 선언된 시점의 상위 스코프의 this를 그대로 사용 **합니다.
const user = {
name: 'Alice',
greet() {
// 일반 함수: this는 user
const inner = () => {
// 화살표 함수: 상위 스코프(greet)의 this를 상속 → user
console.log(this.name);
};
inner();
}
};
user.greet(); // 'Alice'
화살표 함수와 메서드 — 주의할 점
const user = {
name: 'Alice',
// 화살표 함수로 메서드 정의 — 안티패턴!
greet: () => {
console.log(this.name); // this는 상위 스코프(전역)
}
};
user.greet(); // undefined
객체 리터럴은 스코프를 만들지 않으므로, 화살표 함수의 상위 스코프는 전역입니다. ** 메서드 정의에는 일반 함수(축약 메서드)를 사용 **해야 합니다.
화살표 함수에 call/bind는 무의미
const arrow = () => console.log(this);
arrow.call({ name: 'Alice' }); // 전역 객체 — call 무시됨
const bound = arrow.bind({ name: 'Bob' });
bound(); // 전역 객체 — bind 무시됨
화살표 함수는 this를 바인딩하는 메커니즘 자체가 없으므로, call/apply/bind가 this에 영향을 주지 않습니다.
클래스에서의 this
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// 화살표 함수로 this를 렉시컬하게 유지
setInterval(() => {
this.seconds++; // Timer 인스턴스를 가리킴
console.log(this.seconds);
}, 1000);
}
}
const timer = new Timer();
timer.start();
만약 setInterval에 일반 함수를 넣으면 this는 전역 객체가 됩니다. 이것이 이벤트 핸들러나 콜백에서 화살표 함수를 많이 쓰는 이유입니다.
클래스 필드와 화살표 함수
class Button {
// 클래스 필드에 화살표 함수 할당 — 인스턴스마다 새 함수 생성
handleClick = () => {
console.log(this); // 항상 Button 인스턴스
};
}
const btn = new Button();
const fn = btn.handleClick;
fn(); // Button 인스턴스 — 참조 손실 없음
React에서 이벤트 핸들러를 이런 식으로 작성하는 이유가 바로 this 바인딩 문제를 피하기 위해서입니다.
this 판별 순서
코드에서 this가 무엇인지 판별하는 순서입니다.
- ** 화살표 함수인가?** → 상위 스코프의 this
- new로 호출했는가? → 새로 생성된 객체
- call/apply/bind를 사용했는가? → 지정된 객체
- ** 객체의 메서드로 호출했는가?** → 그 객체
- ** 그 외** → 전역 객체(비엄격) 또는 undefined(엄격)
const obj = {
value: 42,
getValue() { return this.value; },
getValueArrow: () => this.value
};
console.log(obj.getValue()); // 42 — 암묵적 바인딩
console.log(obj.getValueArrow()); // undefined — 전역의 this
const get = obj.getValue;
console.log(get()); // undefined — 기본 바인딩
console.log(obj.getValue.call({ value: 100 })); // 100 — 명시적 바인딩
주의할 점
메서드를 콜백으로 전달할 때 this가 풀린다
setTimeout(user.greet, 1000)처럼 메서드를 콜백으로 넘기면, 호출 시점에 . 앞에 객체가 없으므로 기본 바인딩이 적용됩니다. bind로 this를 고정하거나, 화살표 함수로 감싸야 합니다.
객체 리터럴에서 화살표 함수로 메서드를 정의하면 안 된다
객체 리터럴은 스코프를 생성하지 않습니다. 화살표 함수의 상위 스코프가 전역이 되어, this가 전역 객체(또는 undefined)를 가리킵니다. 메서드 정의에는 반드시 축약 메서드 문법을 사용해야 합니다.
bind된 함수의 this는 다시 바꿀 수 없다
bind로 고정한 this는 call이나 apply로도 변경되지 않습니다(하드 바인딩). 단, new 연산자는 bind보다 우선순위가 높아서 새 객체를 this로 만듭니다.
정리
| 항목 | 설명 |
|---|---|
| 기본 바인딩 | 전역 객체(비엄격) 또는 undefined(엄격) |
| 암묵적 바인딩 | . 앞의 객체가 this |
| 명시적 바인딩 | call(개별 인자), apply(배열), bind(새 함수 반환) |
| new 바인딩 | 새로 생성된 객체가 this |
| 화살표 함수 | 자체 this 없음, 상위 스코프에서 렉시컬하게 상속 |
| 우선순위 | new > bind > 암묵적 > 기본 |