프로토타입 상속이란?
프로토타입 상속은 JavaScript의 핵심 메커니즘 중 하나로, 객체가 다른 객체로부터 속성과 메서드를 상속받을 수 있게 해줍니다. Java나 C++과 같은 클래스 기반 언어와 달리, JavaScript는 프로토타입 기반 언어입니다. 이는 객체가 직접 다른 객체로부터 상속받는다는 의미입니다.
기본 프로토타입 상속
가장 간단한 프로토타입 상속 예제부터 시작해 보겠습니다.
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')); // falseconsole.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); // trueconsole.log(Object.getPrototypeOf(parent) === grandparent); // trueconsole.log(Object.getPrototypeOf(grandparent) === Object.prototype); // trueconsole.log(Object.getPrototypeOf(Object.prototype)); // nullObject.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프로토타입 상속 구현하기
생성자 함수를 사용하여 상속을 구현하는 방법입니다.
// 부모 생성자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); // trueconsole.log(myCar instanceof Vehicle); // trueconsole.log(myCar instanceof Object); // trueES6 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); // trueconsole.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); // 2console.log(child.x); // undefinedhasOwnProperty()와 in 연산자
const parent = { inherited: true };const child = Object.create(parent);child.own = true;
// hasOwnProperty: 자신의 속성만 체크console.log(child.hasOwnProperty('own')); // trueconsole.log(child.hasOwnProperty('inherited')); // false
// in 연산자: 프로토타입 체인 전체를 체크console.log('own' in child); // trueconsole.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의 동작 원리를 더 깊이 이해하고 효율적인 코드를 작성할 수 있습니다.