JavaScript의 프로토타입과 프로토타입 체인
JavaScript는 Java나 C++와 같은 클래스 기반 언어가 아닌 프로토타입 기반 언어입니다. 이번 글에서는 JavaScript의 프로토타입이 무엇인지, 그리고 프로토타입 체인이 어떻게 동작하는지 알아보겠습니다.
프로토타입이란?
MDN에서는 프로토타입을 다음과 같이 설명합니다.
프로토타입 기반 언어란 모든 객체가 메소드와 속성을 상속받기 위한 템플릿으로써 프로토타입 객체를 가지는 것을 의미한다.
JavaScript에서 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이는 다른 객체에 대한 참조를 저장합니다.
객체의 속성이나 메서드에 접근할 때 해당 객체에 없으면 [[Prototype]]이 가리키는 객체에서 찾게 됩니다.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`안녕하세요, ${this.name}입니다.`);
};
const person1 = new Person('김철수');
person1.greet(); // 안녕하세요, 김철수입니다.
위 코드에서 person1 객체는 greet 메서드를 직접 가지고 있지 않습니다.
하지만 Person.prototype에 정의된 greet 메서드를 프로토타입 체인을 통해 사용할 수 있습니다.
prototype 프로퍼티와 __proto__
프로토타입을 이해할 때 가장 헷갈리는 부분이 prototype과 __proto__의 차이입니다.
이름이 비슷해 보이지만 둘은 서로 다른 역할을 합니다.
prototype은 생성자 함수가 가지는 프로퍼티입니다.
new 키워드로 인스턴스를 생성할 때, 이 prototype 객체가 인스턴스의 프로토타입으로 설정됩니다.
반면 __proto__는 모든 객체가 가지는 프로퍼티로, 해당 객체의 프로토타입(즉, [[Prototype]] 내부 슬롯)에 접근할 수 있게 해줍니다.
function Person(name) {
this.name = name;
}
const person1 = new Person('김철수');
// Person 생성자 함수의 prototype 프로퍼티
console.log(Person.prototype); // {constructor: ƒ}
// person1 인스턴스의 프로토타입 ([[Prototype]] 내부 슬롯)
console.log(person1.__proto__); // {constructor: ƒ}
// 둘은 같은 객체를 참조합니다
console.log(person1.__proto__ === Person.prototype); // true
즉, 생성자 함수의 prototype이 인스턴스의 __proto__가 되는 것입니다.
다만 __proto__는 원래 비표준이었기 때문에, ES6부터는 Object.getPrototypeOf()를 사용하는 것을 권장하고 있습니다.
// 표준 방식으로 프로토타입 접근
Object.getPrototypeOf(person1) === Person.prototype; // true
프로토타입 체인
프로토타입 체인은 객체의 속성이나 메서드에 접근할 때 해당 객체에서 찾지 못하면 프로토타입을 따라 올라가며 탐색하는 메커니즘입니다.
MDN에서는 프로토타입 체인을 다음과 같이 설명합니다.
객체의 속성에 접근할 때, JavaScript는 객체 자신의 속성을 먼저 확인하고, 없으면
[[Prototype]]을 확인합니다. 이 과정은null에 도달할 때까지 계속됩니다.
const o = {
a: 1,
b: 2,
__proto__: {
b: 3,
c: 4,
},
};
// 프로토타입 체인:
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null
console.log(o.a); // 1 (자체 속성)
console.log(o.b); // 2 (자체 속성, 프로토타입의 b는 가려짐)
console.log(o.c); // 4 (프로토타입에서 발견)
console.log(o.d); // undefined (체인 끝까지 찾지 못함)
위 예시에서 o.b는 2가 출력됩니다.
프로토타입에도 b: 3이 있지만, 객체 자신의 속성이 우선하기 때문입니다.
이를 속성 가려짐(Property Shadowing)이라고 합니다.
프로토타입 체인의 끝
모든 프로토타입 체인은 최종적으로 null에서 끝납니다.
const obj = { a: 1 };
// obj ---> Object.prototype ---> null
console.log(Object.getPrototypeOf(obj)); // Object.prototype
console.log(Object.getPrototypeOf(Object.prototype)); // null
Object.prototype은 대부분의 객체가 가지는 프로토타입 체인의 최상위 객체입니다.
hasOwnProperty(), toString() 같은 메서드는 바로 이 Object.prototype에 정의되어 있어서 모든 객체에서 사용할 수 있습니다.
const person = { name: '김철수' };
// Object.prototype의 메서드들
person.hasOwnProperty('name'); // true
person.toString(); // "[object Object]"
왜 프로토타입 기반으로 만들어졌을까?
프로토타입 기반 설계의 핵심은 메모리 효율성입니다.
만약 메서드를 생성자 함수 내부에서 정의하면 어떻게 될까요?
function Person(name) {
this.name = name;
this.greet = function () {
console.log(`안녕하세요, ${this.name}입니다.`);
};
}
const person1 = new Person('김철수');
const person2 = new Person('이영희');
console.log(person1.greet === person2.greet); // false
위 코드에서 person1.greet과 person2.greet은 서로 다른 함수입니다.
인스턴스를 생성할 때마다 동일한 기능을 하는 함수가 새로 만들어지는 것입니다.
1000개의 인스턴스를 만들면 똑같은 함수가 1000개 생성되어 메모리를 낭비하게 됩니다.
프로토타입을 사용하면 이 문제를 해결할 수 있습니다.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`안녕하세요, ${this.name}입니다.`);
};
const person1 = new Person('김철수');
const person2 = new Person('이영희');
console.log(person1.greet === person2.greet); // true
이제 greet 함수는 프로토타입에 단 하나만 존재하고, 모든 인스턴스가 이를 공유합니다.
그렇다면 ES6에서 도입된 class 문법은 어떨까요?
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`안녕하세요, ${this.name}입니다.`);
}
}
위 코드는 Java나 C++의 클래스처럼 보이지만, JavaScript 엔진은 이를 다음과 같이 해석합니다.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`안녕하세요, ${this.name}입니다.`);
};
실제로 둘이 같은 구조인지 확인해볼 수 있습니다.
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`안녕하세요, ${this.name}입니다.`);
}
}
const person1 = new Person('김철수');
// class의 메서드는 prototype에 저장됩니다
console.log(Person.prototype.greet); // [Function: greet]
console.log(person1.__proto__ === Person.prototype); // true
즉, class는 프로토타입 기반 코드를 더 읽기 쉽게 작성할 수 있도록 도와주는 **문법적 설탕(Syntactic Sugar)**입니다. 겉모습만 클래스처럼 보일 뿐, 내부 동작은 여전히 프로토타입 기반입니다.
따라서 class 문법을 사용하더라도 프로토타입을 이해하고 있어야 JavaScript가 어떻게 동작하는지 정확히 파악할 수 있습니다.