5.2 프로토타입 체인
JavaScript의 상속은 클래스 기반이 아닌 프로토타입 기반입니다. 클래스 문법도 내부적으로는 프로토타입 체인 위에서 동작합니다.
[[Prototype]] 내부 슬롯
모든 JavaScript 객체는 숨겨진 내부 슬롯 [[Prototype]]을 가집니다. 이 슬롯은 다른 객체를 가리키거나 null입니다.
const obj = { x: 1 };
// [[Prototype]] 접근 방법
// 방법 1: __proto__ (비표준, 브라우저에서 지원)
console.log(obj.__proto__ === Object.prototype); // true
// 방법 2: Object.getPrototypeOf() (권장)
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
// 프로토타입 체인 최상위
console.log(Object.getPrototypeOf(Object.prototype)); // null
// 프로토타입 체인 시각화
function getProtoChain(obj) {
const chain = [];
let current = obj;
while (current !== null) {
chain.push(current.constructor?.name ?? "Object");
current = Object.getPrototypeOf(current);
}
return chain.join(" → ");
}
function Animal(name) { this.name = name; }
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const dog = new Dog("바둑이", "진돗개");
console.log(getProtoChain(dog));
// "Dog → Animal → Object"
Object.create()로 프로토타입 설정
// 프로토타입 지정해서 객체 생성
const vehicleProto = {
start() {
return `${this.make} ${this.model} 시동 켜짐`;
},
stop() {
return `${this.make} ${this.model} 시동 꺼짐`;
},
toString() {
return `[Vehicle: ${this.make} ${this.model}]`;
},
};
const car = Object.create(vehicleProto);
car.make = "현대";
car.model = "아반떼";
car.year = 2024;
console.log(car.start()); // "현대 아반떼 시동 켜짐"
console.log(Object.getPrototypeOf(car) === vehicleProto); // true
// Object.create의 두 번째 인수: 프로퍼티 디스크립터
const car2 = Object.create(vehicleProto, {
make: { value: "기아", writable: true, enumerable: true, configurable: true },
model: { value: "K5", writable: true, enumerable: true, configurable: true },
});
console.log(car2.start()); // "기아 K5 시동 켜짐"
// 프로토타입 체인으로 상속 구현
const electricVehicleProto = Object.create(vehicleProto, {
charge: {
value: function() { return `${this.make} ${this.model} 충전 중`; },
writable: true, enumerable: true, configurable: true,
},
});
const ev = Object.create(electricVehicleProto);
ev.make = "테슬라";
ev.model = "Model 3";
console.log(ev.start()); // "테슬라 Model 3 시동 켜짐" (vehicleProto에서 상속)
console.log(ev.charge()); // "테슬라 Model 3 충전 중"
프로토타입 체인에서 프로퍼티 탐색
function Animal(name) {
this.name = name; // 인스턴스 프로퍼티
}
Animal.prototype.type = "동물"; // 프로토타입 프로퍼티
Animal.prototype.breathe = function() { // 프로토타입 메서드
return `${this.name}이 호흡합니다`;
};
function Dog(name, breed) {
Animal.call(this, name); // 부모 생성자 호출
this.breed = breed;
}
// Dog의 프로토타입을 Animal 인스턴스로 설정
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // constructor 복원
Dog.prototype.bark = function() {
return `${this.name}이 짖습니다: 멍멍!`;
};
const dog = new Dog("바둑이", "진돗개");
// 프로퍼티 탐색 순서:
// 1. dog 인스턴스 자신 → name, breed (있음)
// 2. Dog.prototype → constructor, bark (있음)
// 3. Animal.prototype → type, breathe (있음)
// 4. Object.prototype → hasOwnProperty, toString 등
// 5. null → 탐색 종료
console.log(dog.name); // "바둑이" (인스턴스)
console.log(dog.breed); // "진돗개" (인스턴스)
console.log(dog.type); // "동물" (Animal.prototype)
console.log(dog.bark()); // "바둑이이 짖습니다: 멍멍!" (Dog.prototype)
console.log(dog.breathe()); // "바둑이이 호흡합니다" (Animal.prototype)
console.log(dog.toString()); // "[object Object]" (Object.prototype)
// 체인 확인
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
hasOwnProperty vs in 연산자
const parent = { inherited: "부모 값" };
const child = Object.create(parent);
child.own = "자신의 값";
// in 연산자: 프로토타입 체인 전체 검색
console.log("own" in child); // true (자신)
console.log("inherited" in child); // true (프로토타입)
console.log("missing" in child); // false
// hasOwnProperty: 자신의 프로퍼티만
console.log(child.hasOwnProperty("own")); // true
console.log(child.hasOwnProperty("inherited")); // false
// 모던 방식: Object.hasOwn() (ES2022, hasOwnProperty 보다 안전)
console.log(Object.hasOwn(child, "own")); // true
console.log(Object.hasOwn(child, "inherited")); // false
// for...in vs Object.keys
const obj = Object.create({ protoKey: "프로토 값" });
obj.ownKey1 = "자신 값1";
obj.ownKey2 = "자신 값2";
// for...in: 열거 가능한 프로퍼티 모두 (상속 포함)
const forInKeys = [];
for (const key in obj) {
forInKeys.push(key);
}
console.log(forInKeys); // ["ownKey1", "ownKey2", "protoKey"]
// 자신의 것만 필터
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
console.log(`own: ${key}`);
}
}
// Object.keys: 자신의 열거 가능 프로퍼티만
console.log(Object.keys(obj)); // ["ownKey1", "ownKey2"]
프로토타입 오염 방지 패턴
// ❌ 프로토타입 오염 취약점 (절대 사용 금지)
const payload = JSON.parse('{"__proto__": {"admin": true}}');
// Object.assign 사용 시 취약
// Object.assign({}, payload); // 모든 객체에 admin: true 추가됨!
// ✅ 안전한 병합 — 프로토타입 키 필터링
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue; // 위험한 키 건너뜀
}
target[key] = source[key];
}
return target;
}
// ✅ null 프로토타입 객체 사용 (오염 불가)
const safeMap = Object.create(null);
safeMap.user = "Alice";
safeMap.__proto__ = "문자열"; // 단순 키로 취급됨 (실제 프로토타입 변경 없음)
console.log(Object.getPrototypeOf(safeMap)); // null
ES6 클래스와 프로토타입의 관계
클래스는 프로토타입의 문법적 설탕(Syntactic Sugar)입니다.
// ES6 클래스
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name}이 소리를 냅니다`;
}
static create(name) {
return new Animal(name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return super.speak() + " 멍멍!";
}
}
// 내부 구조 확인 — 여전히 프로토타입 기반
console.log(typeof Animal); // "function"
console.log(Animal.prototype.constructor === Animal); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
const dog = new Dog("바둑이", "진돗개");
console.log(dog.speak()); // "바둑이이 소리를 냅니다 멍멍!"
// 프로토타입에 메서드 직접 추가도 가능 (권장하지 않음)
Animal.prototype.eat = function(food) {
return `${this.name}이 ${food}를 먹습니다`;
};
console.log(dog.eat("사료")); // "바둑이이 사료를 먹습니다" (Dog가 상속)
실전 예제: 믹스인 패턴으로 다중 상속 시뮬레이션
// 기능별 믹스인 정의
const Serializable = {
serialize() {
return JSON.stringify(this);
},
deserialize(json) {
return Object.assign(Object.create(Object.getPrototypeOf(this)), JSON.parse(json));
},
};
const Comparable = {
compareTo(other) {
if (this.id < other.id) return -1;
if (this.id > other.id) return 1;
return 0;
},
equals(other) {
return this.id === other.id;
},
};
const Timestamped = {
createdAt: null,
updatedAt: null,
touch() {
this.updatedAt = new Date().toISOString();
return this;
},
initTimestamp() {
this.createdAt = new Date().toISOString();
this.updatedAt = this.createdAt;
return this;
},
};
// 믹스인 적용 유틸리티
function mixin(target, ...sources) {
Object.assign(target.prototype, ...sources);
return target;
}
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
this.initTimestamp();
}
}
mixin(User, Serializable, Comparable, Timestamped);
const alice = new User(1, "Alice", "alice@example.com");
const bob = new User(2, "Bob", "bob@example.com");
console.log(alice.serialize());
// '{"id":1,"name":"Alice","email":"alice@example.com",...}'
console.log(alice.compareTo(bob)); // -1 (alice.id < bob.id)
console.log(alice.equals(bob)); // false
alice.touch();
console.log(alice.updatedAt !== alice.createdAt); // true (업데이트됨)
고수 팁
팁 1: 프로토타입 메서드 동적 확장
// Array.prototype 확장 예 (실제로는 오염 우려로 권장하지 않음)
// 대신 독립 함수 또는 서브클래스 사용
class SmartArray extends Array {
sum() {
return this.reduce((acc, val) => acc + val, 0);
}
average() {
return this.sum() / this.length;
}
unique() {
return new SmartArray(...new Set(this));
}
}
const arr = new SmartArray(1, 2, 3, 4, 5, 3, 2, 1);
console.log(arr.sum()); // 21
console.log(arr.average()); // 2.625
console.log([...arr.unique()]); // [1, 2, 3, 4, 5]
console.log(arr.filter(x => x > 2) instanceof SmartArray); // true!
팁 2: 프로토타입 체인 성능 최적화
// 체인이 길수록 프로퍼티 탐색 비용 증가
// 자주 접근하는 상속 메서드는 지역 변수에 캐싱
class DeepChain extends Object {
constructor() {
super();
// 자주 호출하는 메서드를 직접 바인딩
this.hasOwnProp = Object.prototype.hasOwnProperty.bind(this);
}
}
// 또는 구조 분해로 추출
const { hasOwnProperty } = Object.prototype;
// 나중에 hasOwnProperty.call(obj, key) 사용
팁 3: Symbol.species로 파생 클래스 제어
class SafeArray extends Array {
// map, filter 등이 SafeArray 대신 Array를 반환하도록 설정
static get [Symbol.species]() {
return Array;
}
safeMethod() {
return "SafeArray 전용 메서드";
}
}
const sa = new SafeArray(1, 2, 3);
const mapped = sa.map(x => x * 2); // map은 Array를 반환
console.log(mapped instanceof SafeArray); // false (Array 반환)
console.log(mapped instanceof Array); // true