자바스크립트 고급 #3 프로토타입 체인
자바스크립트의 객체 모델은 다른 OOP 언어들과 달라요. **클래스 기반(class-based)**이 아니라 프로토타입 기반(prototype-based) 입니다. ES2015의 class 문법은 프로토타입 위에 입혀진 설탕(syntactic sugar)입니다. 이번 글은 그 안쪽을 들여다봅니다.
모든 객체는 다른 객체를 참조한다 #
자바스크립트의 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가집니다. 이게 다른 객체를 가리켜요.
const obj = { a: 1 };
Object.getPrototypeOf(obj); // Object.prototype
빈 객체 {}의 프로토타입은 Object.prototype 이라는 빌트인 객체. 거기에는 toString, hasOwnProperty 같은 메서드가 들어있습니다.
const obj = { a: 1 };
obj.toString(); // '[object Object]'
// ↑ obj 에 직접 없지만 Object.prototype 에 있음
obj.toString을 찾을 때 — 자바스크립트는 먼저 obj 자체를 보고, 없으면 프로토타입 체인을 따라 위로 올라가며 찾습니다. Object.prototype.toString까지 가서 발견.
이걸 프로토타입 체인 룩업 이라 부릅니다. 자바스크립트의 거의 모든 메서드 호출이 이 동작 위에서 일어나요.
함수의 prototype 속성
#
함수는 모두 prototype 이라는 속성을 가집니다.
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log(`안녕, ${this.name}`);
};
const u = new User('커티스');
u.greet(); // 안녕, 커티스
new User('커티스')가 실행되면:
- 새 빈 객체를 만듦
- 그 객체의
[[Prototype]]을User.prototype으로 설정 User본문을 실행하면서this에 속성 추가- 객체를 반환
그래서 u.greet을 부르면 — u 에는 없지만 프로토타입 체인을 따라 User.prototype.greet을 찾아 호출합니다.
클래스 = 프로토타입의 설탕 #
ES2015의 class 문법으로 같은 코드를 다시 적으면:
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`안녕, ${this.name}`);
}
}
const u = new User('커티스');
console.log(u); // User { name: '커티스' }
console.log(u.greet); // [Function: greet]
내부적으로 정확히 같은 일이 일어나요.
class User {
greet() { /* ... */ }
}
// 위는 사실상 다음과 같음
function User() {}
User.prototype.greet = function() { /* ... */ };class가 등장한 뒤로 직접 prototype에 메서드를 다는 코드는 거의 없어졌지만, 이 구조는 그대로입니다.
프로토타입 체인 — 깊이 들어가기 #
상속 관계는 체인이 길어지는 것으로 표현됩니다.
class Animal {
speak() { console.log('소리'); }
}
class Dog extends Animal {
bark() { console.log('멍멍'); }
}
const d = new Dog();
d.bark(); // 'd' → 'Dog.prototype' 에서 발견
d.speak(); // 'd' → 'Dog.prototype' (없음) → 'Animal.prototype' 에서 발견
체인:
d
↓ [[Prototype]]
Dog.prototype (bark)
↓ [[Prototype]]
Animal.prototype (speak)
↓ [[Prototype]]
Object.prototype (toString, hasOwnProperty)
↓
null속성을 찾을 때 이 체인을 위로 올라가며 검색합니다. 끝까지 못 찾으면 undefined.
체인 끝은 null
#
Object.getPrototypeOf(Object.prototype); // null
Object.prototype의 프로토타입은 null 입니다. 여기서 체인이 끝나요.
자기 속성 vs 상속된 속성 #
class User {
constructor(name) {
this.name = name;
}
greet() {}
}
const u = new User('커티스');
Object.hasOwn(u, 'name'); // true — 인스턴스 자기 속성
Object.hasOwn(u, 'greet'); // false — 프로토타입에 있음
'greet' in u; // true ('in' 은 체인까지 검사)
Object.hasOwn은 자기 속성만 검사. in 연산자는 체인까지 따라갑니다. 둘의 차이를 의식해야 할 때가 가끔 있습니다.
옛 코드에는 obj.hasOwnProperty('key')가 자주 보이는데, 이건 obj 자체에 hasOwnProperty가 정의되어 있으면 깨질 수 있습니다. ES2022의 Object.hasOwn이 안전한 답입니다.
프로토타입을 직접 다루는 도구 #
1) Object.create — 프로토타입을 명시해 만들기
#
const animalProto = {
speak() { console.log('소리'); },
};
const dog = Object.create(animalProto);
dog.bark = function() { console.log('멍멍'); };
dog.speak(); // 소리 (프로토타입에서)
dog.bark(); // 멍멍 (자기 속성)
클래스 없이 프로토타입을 직접 정해서 객체를 만드는 방법. 옛 자료에서 자주 보이지만, 모던 코드에서는 거의 class가 표준입니다.
2) Object.create(null) — 프로토타입 없는 객체
#
const dict = Object.create(null);
dict.toString; // undefined — Object.prototype 도 없음
const normal = {};
normal.toString; // [Function: toString] — 빌트인이 보임
키-값 사전으로 객체를 쓸 때 — toString 같은 빌트인 키 충돌을 막으려면 Object.create(null)이 안전합니다. 모던 자바스크립트에는 Map이 더 좋은 선택지 이지만, 라이브러리 내부 코드에서 가끔 만납니다.
프로토타입 수정 — 권장하지 않음 #
Array.prototype.first = function() {
return this[0];
};
[1, 2, 3].first(); // 1
빌트인의 프로토타입에 메서드를 더하는 패턴(이른바 monkey patching)은 매우 강력하지만 거의 항상 사고를 만듭니다. 다른 코드(라이브러리, 표준 신규 메서드)와 충돌하기 쉬워요.
자기 클래스의 prototype에는 평범하게 메서드를 추가할 수 있고 그게 정상입니다. 빌트인을 건드리는 건 피하세요.
__proto__ — 옛 동작
#
옛 자바스크립트에는 __proto__ 라는 비공식 속성이 있었습니다. ES2015에서 표준화됐지만 사용은 권장되지 않습니다.
const obj = {
__proto__: animalProto, // 사용하지 마세요
};
// 권장
const obj2 = Object.create(animalProto);Object.getPrototypeOf / Object.setPrototypeOf / Object.create가 모던 정답입니다.
instanceof — 프로토타입 체인 검사
#
class Animal {}
class Dog extends Animal {}
const d = new Dog();
d instanceof Dog; // true
d instanceof Animal; // true (체인을 따라 부모도)
d instanceof Object; // true
x instanceof C는 C.prototype이 x의 프로토타입 체인 어딘가에 있는가를 검사합니다. 체인 룩업의 또 다른 응용입니다.
함수도 객체 — 자기 속성 가질 수 있음 #
자바스크립트에서 함수는 객체이기도 합니다. 속성을 직접 달 수 있습니다.
function counter() {
counter.count = (counter.count ?? 0) + 1;
return counter.count;
}
counter(); // 1
counter(); // 2
counter(); // 3
이런 패턴은 잘 쓰지 않지만, 자바스크립트의 함수가 1급 객체(first-class object)라는 의미가 드러나는 예시입니다.
구체적인 예 — 배열의 메서드 체인 #
const arr = [1, 2, 3];
Object.hasOwn(arr, 'map'); // false
Object.hasOwn(arr, 'length'); // true
arr.__proto__ === Array.prototype; // true
Array.prototype.__proto__ === Object.prototype; // true
체인:
[1, 2, 3] (length, 0, 1, 2)
↓
Array.prototype (map, filter, reduce, ...)
↓
Object.prototype (toString, ...)
↓
nullarr.map을 호출하면 — arr 자체에는 없으니 Array.prototype에서 찾아 실행합니다. 모든 배열이 같은 Array.prototype을 공유하니 메서드가 한 번만 메모리에 있습니다.
마무리 #
이번 글에서 정리한 내용:
- 자바스크립트는 프로토타입 기반 — 클래스 문법은 설탕
- 모든 객체는
[[Prototype]]으로 다른 객체를 가리킴 - 속성을 찾을 때 프로토타입 체인을 위로 따라감
- 함수의
prototype속성이new호출 시 인스턴스의[[Prototype]]으로 설정 Object.hasOwn으로 자기 속성 검사 (in은 체인까지)Object.create(...)로 프로토타입을 명시해 만들기- 빌트인 prototype 수정은 권장 X
instanceof는 프로토타입 체인 검사
다음 글(#4 이벤트 루프와 태스크)에서는 자바스크립트 비동기의 실제 동작 — 이벤트 루프, 마이크로태스크와 매크로태스크의 차이, 그리고 Promise.then과 setTimeout이 왜 다른 순서로 실행되는지를 다룹니다.