JavaScriptHard#04
프로토타입 체인(Prototype Chain)이란 무엇이며 상속과 어떤 관련이 있나요?
#JS#프로토타입#상속#OOP
힌트
__proto__와 prototype 속성의 차이를 생각해보세요.
정답 및 해설
프로토타입 체인(Prototype Chain)이란 무엇이며 상속과 어떤 관련이 있나요?
JavaScript는 클래스 기반 언어(Java, C++)와 달리 프로토타입 기반(Prototype-based) 언어입니다. 객체가 다른 객체를 직접 참조하는 방식으로 속성과 메서드를 공유하고 상속합니다. ES6에서 도입된 class 문법도 내부적으로는 프로토타입 메커니즘을 기반으로 작동하는 문법적 설탕(syntactic sugar)입니다.
[[Prototype]] 내부 슬롯
모든 JavaScript 객체는 [[Prototype]]이라는 내부 슬롯을 가집니다. 이 슬롯은 다른 객체를 참조하거나 null을 가리킵니다. 이 참조를 통해 연결된 객체들의 사슬이 프로토타입 체인입니다.
const obj = { name: 'Alice' };
// __proto__는 [[Prototype]]에 접근하는 접근자 프로퍼티 (비표준이지만 널리 지원됨)
console.log(obj.__proto__ === Object.prototype); // true
// 표준 방법
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
프로퍼티 탐색 과정
객체에서 프로퍼티나 메서드를 찾을 때 JavaScript 엔진은 다음 순서로 탐색합니다.
- 현재 객체에서 찾는다.
- 없으면
[[Prototype]]이 가리키는 객체에서 찾는다. - 없으면 그 객체의
[[Prototype]]에서 찾는다. null에 도달할 때까지 반복한다.null에 도달하면undefined를 반환한다.
const animal = {
breathe() {
return '숨을 쉽니다';
}
};
const dog = {
bark() {
return '멍멍!';
}
};
// dog의 [[Prototype]]을 animal로 설정
Object.setPrototypeOf(dog, animal);
console.log(dog.bark()); // '멍멍!' — dog 자신의 메서드
console.log(dog.breathe()); // '숨을 쉽니다' — [[Prototype]]인 animal에서 찾음
console.log(dog.fly); // undefined — 어디서도 찾지 못함
생성자 함수와 prototype 프로퍼티
함수는 prototype 프로퍼티를 가집니다. new 키워드로 객체를 생성하면, 생성된 객체의 [[Prototype]]이 생성자 함수의 prototype 객체를 가리킵니다.
function Person(name, age) {
this.name = name;
this.age = age;
}
// prototype 객체에 메서드 추가
Person.prototype.greet = function () {
return `안녕하세요, 저는 ${this.name}입니다.`;
};
const alice = new Person('Alice', 25);
const bob = new Person('Bob', 30);
console.log(alice.greet()); // '안녕하세요, 저는 Alice입니다.'
console.log(bob.greet()); // '안녕하세요, 저는 Bob입니다.'
// alice와 bob은 같은 greet 함수를 공유 (메모리 절약)
console.log(alice.greet === bob.greet); // true
// 프로토타입 체인 확인
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
메모리 관점에서의 장점
// 나쁜 예 — 각 인스턴스마다 함수가 별도로 생성됨
function PersonBad(name) {
this.name = name;
this.greet = function() { return `Hi, ${this.name}`; }; // 매번 새 함수 생성
}
// 좋은 예 — 모든 인스턴스가 프로토타입의 함수를 공유
function PersonGood(name) {
this.name = name;
}
PersonGood.prototype.greet = function() { return `Hi, ${this.name}`; };
프로토타입 체인을 통한 상속
생성자 함수를 이용한 상속
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name}이(가) 소리를 냅니다.`;
};
function Dog(name, breed) {
Animal.call(this, name); // 부모 생성자 호출
this.breed = breed;
}
// Dog의 prototype을 Animal의 인스턴스로 설정 (프로토타입 체인 연결)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // constructor 복원
Dog.prototype.bark = function () {
return `${this.name}: 멍멍!`;
};
const rex = new Dog('Rex', '진돗개');
console.log(rex.bark()); // 'Rex: 멍멍!' — Dog.prototype에서
console.log(rex.speak()); // 'Rex이(가) 소리를 냅니다.' — Animal.prototype에서
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
// 프로토타입 체인: rex → Dog.prototype → Animal.prototype → Object.prototype → null
ES6 class 문법 (내부적으로 동일)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name}이(가) 소리를 냅니다.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Animal.call(this, name)과 동일
this.breed = breed;
}
bark() {
return `${this.name}: 멍멍!`;
}
}
const rex = new Dog('Rex', '진돗개');
console.log(rex.bark()); // 'Rex: 멍멍!'
console.log(rex.speak()); // 'Rex이(가) 소리를 냅니다.'
// class도 내부적으로 프로토타입을 사용
console.log(Object.getPrototypeOf(Dog) === Animal); // true (정적 상속)
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
Object.create()로 직접 프로토타입 설정
const vehicleProto = {
start() {
return `${this.type} 시동을 겁니다.`;
},
stop() {
return `${this.type} 시동을 끕니다.`;
}
};
const car = Object.create(vehicleProto);
car.type = '자동차';
const bike = Object.create(vehicleProto);
bike.type = '자전거';
console.log(car.start()); // '자동차 시동을 겁니다.'
console.log(bike.start()); // '자전거 시동을 겁니다.'
console.log(Object.getPrototypeOf(car) === vehicleProto); // true
프로퍼티 쉐도잉 (Property Shadowing)
인스턴스에 프로토타입과 같은 이름의 프로퍼티를 추가하면, 인스턴스 자신의 프로퍼티가 프로토타입의 것을 가립니다.
function Shape(color) {
this.color = color;
}
Shape.prototype.describe = function () {
return `색상: ${this.color}`;
};
const circle = new Shape('빨강');
// 인스턴스에 같은 이름의 메서드 추가 (쉐도잉)
circle.describe = function () {
return `원 — ${this.color}`;
};
console.log(circle.describe()); // '원 — 빨강' (쉐도잉된 메서드)
const square = new Shape('파랑');
console.log(square.describe()); // '색상: 파랑' (프로토타입 메서드)
hasOwnProperty와 in 연산자
const person = { name: 'Alice' };
Object.prototype.species = 'human'; // 모든 객체의 프로토타입에 추가
console.log('name' in person); // true (자신 + 프로토타입 체인 전체 탐색)
console.log('species' in person); // true (프로토타입에서 찾음)
console.log(person.hasOwnProperty('name')); // true (자신만 탐색)
console.log(person.hasOwnProperty('species')); // false (자신에게 없음)
// for...in은 프로토타입 체인까지 열거하므로 주의 필요
for (const key in person) {
if (person.hasOwnProperty(key)) {
console.log(key); // 'name'만 출력
}
}
정리
| 개념 | 설명 |
|---|---|
[[Prototype]] | 모든 객체가 가진 내부 슬롯, 다른 객체를 참조 |
| 프로토타입 체인 | [[Prototype]] 참조가 연결된 사슬 |
prototype 프로퍼티 | 함수만 가지는 프로퍼티, new로 생성된 객체의 [[Prototype]]이 됨 |
Object.create() | 특정 객체를 프로토타입으로 하는 새 객체 생성 |
class | 프로토타입 기반 상속의 문법적 설탕 |
| 프로퍼티 쉐도잉 | 인스턴스 프로퍼티가 프로토타입 프로퍼티를 가리는 현상 |
프로토타입 체인은 JavaScript의 가장 핵심적인 특성 중 하나입니다. class 문법을 주로 사용하더라도, 내부 동작 원리를 이해하면 상속 관련 버그를 진단하고 성능을 최적화하는 데 큰 도움이 됩니다.