# JavaScript의 프로토타입 상속: 객체 지향 프로그래밍의 핵심
Table of Contents
프로토타입 상속이란?
프로토타입 상속은 JavaScript의 핵심 메커니즘 중 하나로, 객체가 다른 객체로부터 속성과 메서드를 상속받을 수 있게 해줍니다. Java나 C++과 같은 클래스 기반 언어와 달리, JavaScript는 프로토타입 기반 언어입니다. 이는 객체가 직접 다른 객체로부터 상속받는다는 의미입니다.
:::important
JavaScript의 모든 객체는 내부적으로 [[Prototype]]이라는 숨겨진 속성을 가지고 있습니다. 이 속성은 다른 객체를 참조하거나 null을 가질 수 있습니다. 이것이 바로 프로토타입 체인의 기반입니다.
:::
기본 프로토타입 상속
가장 간단한 프로토타입 상속 예제부터 시작해 보겠습니다.
const parent = {
greet() {
console.log('Hello from parent!');
},
name: 'Parent Object'
};
// parent를 프로토타입으로 가지는 child 객체 생성
const child = Object.create(parent);
child.greet(); // "Hello from parent!"
console.log(child.name); // "Parent Object"
// child 객체 자체에는 greet 메서드가 없습니다
console.log(child.hasOwnProperty('greet')); // false
console.log(child.hasOwnProperty('name')); // false
// 하지만 프로토타입 체인을 통해 접근할 수 있습니다
console.log('greet' in child); // true
프로토타입 체인의 동작 원리
JavaScript에서 객체의 속성이나 메서드에 접근하려고 할 때, 엔진은 다음과 같은 순서로 검색합니다.
- 객체 자체에서 속성을 찾습니다.
- 없으면 객체의 프로토타입에서 찾습니다.
- 프로토타입의 프로토타입에서 찾습니다.
- 프로토타입 체인의 끝(
null)에 도달할 때까지 계속됩니다.
const grandparent = {
familyName: 'Kim',
heritage() {
return `우리 집안은 ${this.familyName}입니다.`;
}
};
const parent = Object.create(grandparent);
parent.occupation = 'Engineer';
const child = Object.create(parent);
child.age = 10;
console.log(child.age); // 10 (자신의 속성)
console.log(child.occupation); // "Engineer" (부모로부터)
console.log(child.familyName); // "Kim" (조부모로부터)
console.log(child.heritage()); // "우리 집안은 Kim입니다."
// 프로토타입 체인 확인
console.log(Object.getPrototypeOf(child) === parent); // true
console.log(Object.getPrototypeOf(parent) === grandparent); // true
console.log(Object.getPrototypeOf(grandparent) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
Object.create()를 사용한 상속
Object.create()는 지정된 프로토타입 객체를 가진 새 객체를 생성하는 가장 명확한 방법입니다.
// 기본 사용법
const animal = {
type: 'Animal',
describe() {
return `This is a ${this.type}`;
}
};
const dog = Object.create(animal);
dog.type = 'Dog';
console.log(dog.describe()); // "This is a Dog"
// 속성 디스크립터와 함께 사용
const cat = Object.create(animal, {
type: {
value: 'Cat',
writable: true,
enumerable: true,
configurable: true
},
meow: {
value: function() {
return '야옹!';
},
enumerable: true
}
});
console.log(cat.describe()); // "This is a Cat"
console.log(cat.meow()); // "야옹!"
생성자 함수와 프로토타입
ES6 이전에는 생성자 함수와 프로토타입을 사용하여 객체를 생성했습니다.
function Person(name, age) {
this.name = name;
this.age = age;
}
// 프로토타입에 메서드 추가
Person.prototype.introduce = function() {
return `안녕하세요, 저는 ${this.name}이고 ${this.age}살입니다.`;
};
Person.prototype.greet = function() {
return `안녕하세요!`;
};
const person1 = new Person('홍길동', 30);
const person2 = new Person('김철수', 25);
console.log(person1.introduce()); // "안녕하세요, 저는 홍길동이고 30살입니다."
console.log(person2.introduce()); // "안녕하세요, 저는 김철수이고 25살입니다."
// 모든 인스턴스가 같은 프로토타입을 공유합니다
console.log(person1.introduce === person2.introduce); // true
// prototype 속성 확인
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
:::tip 프로토타입에 메서드를 정의하면 모든 인스턴스가 같은 함수를 공유하므로 메모리를 절약할 수 있습니다. 만약 생성자 함수 내부에서 메서드를 정의하면, 각 인스턴스마다 새로운 함수가 생성됩니다. :::
프로토타입 상속 구현하기
생성자 함수를 사용하여 상속을 구현하는 방법입니다.
// 부모 생성자
function Vehicle(brand) {
this.brand = brand;
this.engineStarted = false;
}
Vehicle.prototype.start = function() {
this.engineStarted = true;
return `${this.brand} 엔진이 시작되었습니다.`;
};
Vehicle.prototype.stop = function() {
this.engineStarted = false;
return `${this.brand} 엔진이 정지되었습니다.`;
};
// 자식 생성자
function Car(brand, model) {
// 부모 생성자 호출
Vehicle.call(this, brand);
this.model = model;
this.wheels = 4;
}
// 프로토타입 체인 설정
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
// Car만의 메서드 추가
Car.prototype.drive = function() {
if (!this.engineStarted) {
return '먼저 엔진을 시작하세요!';
}
return `${this.brand} ${this.model}이(가) 주행 중입니다.`;
};
const myCar = new Car('현대', '소나타');
console.log(myCar.start()); // "현대 엔진이 시작되었습니다."
console.log(myCar.drive()); // "현대 소나타이(가) 주행 중입니다."
console.log(myCar.stop()); // "현대 엔진이 정지되었습니다."
// 인스턴스 체크
console.log(myCar instanceof Car); // true
console.log(myCar instanceof Vehicle); // true
console.log(myCar instanceof Object); // true
:::caution
프로토타입 체인을 설정할 때 반드시 Car.prototype.constructor = Car를 설정해야 합니다. 그렇지 않으면 constructor가 Vehicle을 가리키게 됩니다.
:::
ES6 Class와 프로토타입
ES6의 class 문법은 프로토타입 상속의 문법적 설탕(syntactic sugar)입니다. 훨씬 더 직관적입니다.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name}이(가) 소리를 냅니다.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 부모 클래스의 constructor 호출
this.breed = breed;
}
speak() {
return `${this.name}이(가) 멍멍 짖습니다!`;
}
fetch() {
return `${this.breed} ${this.name}이(가) 공을 가져옵니다.`;
}
}
const dog = new Dog('바둑이', '진돗개');
console.log(dog.speak()); // "바둑이이(가) 멍멍 짖습니다!"
console.log(dog.fetch()); // "진돗개 바둑이이(가) 공을 가져옵니다."
// 내부적으로는 여전히 프로토타입 기반입니다
console.log(typeof Dog); // "function"
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
프로토타입 관련 주요 메서드
Object.getPrototypeOf()와 Object.setPrototypeOf()
const parent = { x: 1 };
const child = Object.create(parent);
console.log(Object.getPrototypeOf(child) === parent); // true
// 프로토타입 변경 (권장하지 않음)
const newParent = { y: 2 };
Object.setPrototypeOf(child, newParent);
console.log(child.y); // 2
console.log(child.x); // undefined
:::warning
Object.setPrototypeOf()는 성능에 큰 영향을 미칠 수 있으므로 사용을 피해야 합니다. 대신 Object.create()를 사용하여 처음부터 올바른 프로토타입으로 객체를 생성하세요.
:::
hasOwnProperty()와 in 연산자
const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;
// hasOwnProperty: 자신의 속성만 체크
console.log(child.hasOwnProperty('own')); // true
console.log(child.hasOwnProperty('inherited')); // false
// in 연산자: 프로토타입 체인 전체를 체크
console.log('own' in child); // true
console.log('inherited' in child); // true
// Object.keys()는 자신의 열거 가능한 속성만 반환
console.log(Object.keys(child)); // ['own']
// for...in은 프로토타입 체인의 열거 가능한 속성도 포함
for (let key in child) {
console.log(key); // 'own', 'inherited'
}
결론
JavaScript의 프로토타입 상속은 강력하고 유연한 메커니즘입니다.
- 객체는 프로토타입 체인을 통해 다른 객체의 기능을 상속받습니다.
- ES6 Class는 프로토타입 상속을 더 쉽게 사용하기 위한 문법입니다.
- 프로토타입을 이해하면 JavaScript의 동작 원리를 더 깊이 이해하고 효율적인 코드를 작성할 수 있습니다.