4.3 this 완전 정복
this는 JavaScript에서 가장 많은 혼란을 일으키는 키워드 중 하나입니다. 핵심 규칙은 단 하나입니다. this는 함수가 호출되는 방식에 따라 결정됩니다 (화살표 함수는 예외).
this 바인딩 4가지 규칙
규칙 1: 전역/일반 함수 호출 (기본 바인딩)
함수를 단독으로 호출하면 this는 전역 객체(브라우저: window, Node.js: globalThis)를 가리킵니다. 엄격 모드에서는 undefined입니다.
function showThis() {
console.log(this);
}
showThis(); // 브라우저: window, Node.js: globalThis
// 엄격 모드
"use strict";
function showThisStrict() {
console.log(this); // undefined
}
showThisStrict();
// 내부 함수에서도 같은 규칙
function outer() {
function inner() {
console.log(this); // window (또는 undefined in strict mode)
}
inner(); // 단독 호출
}
outer();
규칙 2: 메서드 호출 (암시적 바인딩)
객체의 메서드로 호출하면 this는 해당 객체를 가리킵니다. 점(.) 앞의 객체가 this입니다.
const person = {
name: "Alice",
age: 30,
greet() {
console.log(`안녕하세요! ${this.name}입니다.`);
},
getAge: function() {
return this.age;
},
};
person.greet(); // "안녕하세요! Alice입니다."
console.log(person.getAge()); // 30
// 암시적 바인딩 소실 주의!
const greetFunc = person.greet; // 메서드를 변수에 할당
greetFunc(); // "안녕하세요! undefined입니다." — this가 전역으로 바뀜
// 메서드 체이닝
const builder = {
parts: [],
add(part) {
this.parts.push(part);
return this; // this를 반환해야 체이닝 가능
},
build() {
return this.parts.join(", ");
},
};
console.log(builder.add("A").add("B").add("C").build()); // "A, B, C"
규칙 3: new 생성자 호출 (new 바인딩)
new 키워드로 함수를 호출하면 새 빈 객체가 생성되고 this가 그 객체를 가리킵니다.
function Person(name, age) {
// new로 호출 시 this는 새로 생성된 객체
this.name = name;
this.age = age;
this.greet = function() {
return `${this.name} (${this.age}세)`;
};
// 암시적으로 this 반환 (return 불필요)
}
const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);
console.log(alice.greet()); // "Alice (30세)"
console.log(bob.greet()); // "Bob (25세)"
console.log(alice instanceof Person); // true
// new 없이 호출하면 전역 오염!
// const carol = Person("Carol", 20); // 전역에 name, age 추가됨 (strict mode에서 에러)
규칙 4: 명시적 바인딩 (call / apply / bind)
call, apply, bind로 this를 직접 지정합니다.
function introduce(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const alice = { name: "Alice" };
const bob = { name: "Bob" };
// call: 인수를 쉼표로 구분
console.log(introduce.call(alice, "안녕하세요", "!")); // "안녕하세요, Alice!"
console.log(introduce.call(bob, "Hello", ".")); // "Hello, Bob."
// apply: 인수를 배열로 전달
console.log(introduce.apply(alice, ["Hi", "~"])); // "Hi, Alice~"
// bind: 새 함수를 반환 (즉시 실행하지 않음)
const bobIntroduce = introduce.bind(bob);
console.log(bobIntroduce("Good day", "!")); // "Good day, Bob!"
// bind로 인수 부분 적용(Partial Application)
const aliceGreet = introduce.bind(alice, "Hello");
console.log(aliceGreet("!")); // "Hello, Alice!"
console.log(aliceGreet("?")); // "Hello, Alice?"
화살표 함수의 this
화살표 함수는 자신만의 this를 가지지 않습니다. 정의된 시점의 외부 스코프 this를 캡처합니다.
const obj = {
name: "Object",
values: [1, 2, 3],
// 일반 함수 메서드
processRegular() {
// this === obj ✓
return this.values.map(function(v) {
// this === window (또는 undefined in strict mode) ✗
return `${this.name}: ${v}`; // this.name이 undefined
});
},
// 화살표 함수 사용
processArrow() {
// this === obj ✓
return this.values.map((v) => {
// 화살표 함수: 외부 processArrow의 this(obj)를 캡처 ✓
return `${this.name}: ${v}`;
});
},
};
console.log(obj.processRegular()); // ["undefined: 1", "undefined: 2", "undefined: 3"]
console.log(obj.processArrow()); // ["Object: 1", "Object: 2", "Object: 3"]
클래스에서 화살표 함수 활용
class EventHandler {
constructor(name) {
this.name = name;
this.count = 0;
}
// 클래스 필드 화살표 함수: 인스턴스에 바인딩됨
handleClick = () => {
this.count++;
console.log(`${this.name} 클릭 #${this.count}`);
};
// 일반 메서드: 이벤트 리스너로 넘기면 this 소실
handleHover() {
console.log(`${this.name} 호버`); // this.name이 undefined일 수 있음
}
}
const handler = new EventHandler("버튼");
// 이벤트 리스너로 넘겨도 this 유지됨
const clickFn = handler.handleClick;
clickFn(); // "버튼 클릭 #1" — 정상 동작!
const hoverFn = handler.handleHover;
// hoverFn(); // "undefined 호버" — this 소실!
call / apply / bind 상세 비교
const user = {
name: "Alice",
points: 100,
};
function addPoints(amount, bonus = 0) {
this.points += amount + bonus;
return `${this.name}: ${this.points}점`;
}
// call: 즉시 실행, 인수를 나열
console.log(addPoints.call(user, 50, 10)); // "Alice: 160점"
// apply: 즉시 실행, 인수를 배열로
console.log(addPoints.apply(user, [30, 5])); // "Alice: 195점"
// bind: 새 함수 반환 (나중에 실행)
const addPointsToUser = addPoints.bind(user);
console.log(addPointsToUser(20)); // "Alice: 215점"
// bind로 첫 번째 인수만 미리 바인딩
const addFixedBonus = addPoints.bind(user, 0, 100); // amount=0, bonus=100
console.log(addFixedBonus()); // "Alice: 315점"
apply의 실전 활용 — 배열 펼치기
const numbers = [5, 3, 8, 1, 9, 2, 7];
// Math.max는 배열을 직접 받지 못함
// Math.max(numbers) → NaN
// 방법 1: apply
const max1 = Math.max.apply(null, numbers);
// 방법 2: 스프레드 (현대적 방법)
const max2 = Math.max(...numbers);
console.log(max1, max2); // 9 9
이벤트 핸들러에서의 this 함정
class Button {
constructor(label) {
this.label = label;
this.clickCount = 0;
}
// 방법 1: bind 사용 (명시적)
attachWithBind(element) {
element.addEventListener("click", this.handleClick.bind(this));
}
// 방법 2: 화살표 함수 래퍼
attachWithArrow(element) {
element.addEventListener("click", () => this.handleClick());
}
// 방법 3: 클래스 필드 화살표 함수
handleClick = () => {
this.clickCount++;
console.log(`${this.label} 버튼 클릭 ${this.clickCount}회`);
};
}
// DOM이 없는 환경에서 시뮬레이션
const btn = new Button("확인");
const mockElement = {
_handlers: [],
addEventListener(event, handler) {
this._handlers.push(handler);
},
click() {
this._handlers.forEach(h => h());
},
};
btn.attachWithArrow(mockElement);
mockElement.click(); // "확인 버튼 클릭 1회"
mockElement.click(); // "확인 버튼 클릭 2회"
클래스에서 this 안정적 사용 패턴
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.requestCount = 0;
}
// 클래스 필드 화살표 함수: 항상 안전
fetchData = async (endpoint) => {
this.requestCount++;
const url = `${this.baseURL}${endpoint}`;
console.log(`[${this.requestCount}] GET ${url}`);
try {
// 실제 환경에서는 fetch(url) 사용
return { url, count: this.requestCount };
} catch (error) {
throw new Error(`요청 실패: ${error.message}`);
}
};
// 일반 메서드: 직접 호출 시 안전, 분리 시 주의
getStats() {
return { baseURL: this.baseURL, requestCount: this.requestCount };
}
}
const client = new ApiClient("https://api.example.com");
// 구조 분해 후 호출해도 안전 (화살표 함수 필드)
const { fetchData } = client;
fetchData("/users"); // [1] GET https://api.example.com/users
// 배열 메서드에 전달해도 안전
["/posts", "/comments"].forEach(client.fetchData);
// [2] GET https://api.example.com/posts
// [3] GET https://api.example.com/comments
우선순위 규칙 요약
this 바인딩 우선순위 (높음 → 낮음):
new바인딩 —new Fn()호출- 명시적 바인딩 —
call(),apply(),bind() - 암시적 바인딩 —
obj.method()메서드 호출 - 기본 바인딩 — 단독 함수 호출 (전역 또는 undefined)
- 화살표 함수 — 위 규칙 모두 무시, 렉시컬 this 사용
function test() {
console.log(this.x);
}
const obj1 = { x: 1, test };
const obj2 = { x: 2, test };
// new가 bind보다 우선
const BoundTest = test.bind({ x: 99 });
const instance = new BoundTest(); // new: this.x는 undefined (새 객체)
// call이 암시적 바인딩보다 우선
obj1.test.call(obj2); // 2 (obj2의 x)
고수 팁
팁 1: this 바인딩 추적 패턴
function debugThis(label) {
return function() {
console.log(`[${label}] this:`, this?.constructor?.name ?? typeof this);
};
}
const obj = {
regular: debugThis("regular"),
arrow: (() => {
const fn = debugThis("arrow");
return fn; // 이미 전역 this 캡처
})(),
};
팁 2: bind 재바인딩 불가
function greet() { return this.name; }
const alice = { name: "Alice" };
const bob = { name: "Bob" };
const greetAlice = greet.bind(alice);
const tryBob = greetAlice.bind(bob); // bind는 한 번만 적용됨!
console.log(greetAlice()); // "Alice"
console.log(tryBob()); // "Alice" — 재바인딩 불가
console.log(greet.call(bob)); // "Bob" — call은 항상 적용됨
팁 3: Symbol.hasInstance와 instanceof this
class Even {
static [Symbol.hasInstance](value) {
return Number.isInteger(value) && value % 2 === 0;
}
}
console.log(2 instanceof Even); // true
console.log(3 instanceof Even); // false
console.log(10 instanceof Even); // true