이 글은 '이웅모'님의 '모던 자바스크립트 Deep Dive' 책을 통해 공부한 내용을 정리한 글입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.
자바스크립트는 프로로타입 기반의 객체지향 프로그래밍 언어이다. 즉, 자바스크립트는 프로토타입을 기반으로 상속을 구현한다. 이는 클래스 기반 객체지향 프로그래밍 언어보다 더 효율적이며 강력한 객체지향 프로그래밍을 가능케 한다. 다음 코드를 보자.
function Foo(x) {
this.x = x;
this.plusX = function() {
return this.x + this.x;
};
}
const foo1 = new Foo(1);
const foo2 = new Foo(2);
console.log(foo1.plusX === foo2.plusX); //false
생성자 함수 Foo가 생성하는 인스턴스는 x 프로퍼티와 plusX 메소드를 가진다. x 프로퍼티는 일반적으로 다른 값을 갖지만 plusX 메서드는 모든 인스턴스가 동일하게 사용하므로 하나만 생성하여 공유하며 사용하는 것이 바람직하다고 볼 수 있다. 하지만 위 생성자 함수 Foo는 plusX 메서드를 중복 생성하고 모든 인스턴스가 중복 소유한다. 이는 메모리를 불필요하게 낭비하며 인스턴스를 생성할 때 마다 메서드 또한 생성되므로 퍼포먼스에도 악영향을 준다.
function Foo(x) {
this.x = x;
}
Foo.prototype.plusX = function() {
return this.x + this.x
}
const foo1 = new Foo(1);
const foo2 = new Foo(2);
console.log(foo1.plusX === foo2.plusX); //true
위와 같이 프로토타입에 모든 인스턴스가 공통적으로 사용하는 프로퍼티나 메서드를 구현해 두면 모든 인스턴스는 상속을 통해 부모 객체인 프로토타입의 자산을 공유하여 사용할 수 있다. 코드를 그림으로 나타내면 다음과 같다.
그림을 설명하기 전 잠깐 prototype, 내부 슬롯 [[Prototype]]과 접근자 프로퍼티 __proto__에 대해 알아보자.
즉, 생성자 함수 Foo로 생성된 인스턴스 foo1과 foo2는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입인 Foo.prototype에 접근할 수 있으며 Foo.prototype의 프로퍼티를 상속 받는다. 따라서 foo1.plusX라 함은 foo1에 plusX 메서드가 있는 것이 아니라 Foo.prototype의 plusX 메서드를 상속 받아 호출하는 것이다.
또한 모든 프로토타입은 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리키는 constructor 프로퍼티를 갖는다. 프로토타입은 생성자 함수가 생성되는 시점에 생성되며, 생성자 함수와 언제나 쌍으로 존재하기 때문이다.
console.log(foo1.constructor === Foo); // true
객체는 다양한 생성 방법이 존재한다.
각각의 방식으로 생성된 객체는 세부적인 객체 생성 방식의 차이는 있으나 추상 연산 OrdinaryObjectCreate에 의해 생성된다는 공통점이 있다. 간략하게 말하면, 프로토타입은 추상 연산 OrdinaryObjectCreate에 전달되는 인수에 의해 결정된다. 이 인수는 객체가 생성되는 시점에 객체 생성 방식에 의해 결정된다.
const obj1 = new Object();
obj1.a = 1;
const obj2 = { a: 2 };
function Foo(a) {
this.a = a;
}
const obj3 = new Foo(3);
console.log(obj1.constructor === Object); // true
console.log(obj2.constructor === Object); // true
console.log(obj3.constructor === Foo); // true
자바스크립트는 특정 프로퍼티에 접근할 때 해당 객체에 프로퍼티가 없으면 [[Prototype]] 내부 슬롯의 참조를 따라 상위 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라 한다. 이와 비슷한 개념으로 스코프 체인이 있다. 스코프 체인은 식별자 검색을 위한 메커니즘으로 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘으로 이해하면 되겠다. 이 둘은 별도로 동작하는 것이 아니고 서로 협력하여 식별자와 프로퍼티를 검색한다.
function Fruit(name, price) {
this.name = name;
this.price = price;
}
Fruit.prototype.itsPirce = function() {
console.log(`${this.name}'s price is ${this.price}`);
};
const apple = new Fruit("apple", 3);
console.log(apple.hasOwnProperty("name")); // true
console.log(Object.getPrototypeOf(apple) === Fruit.prototype); // true
console.log(Object.getPrototypeOf(Fruit.prototype) === Object.prototype); //true
위 코드를 보면 인스턴스 apple이 Object.prototype의 메서드인 hasOwnProperty를 호출하는 것을 볼 수 있다. 즉, apple 객체는 Fruit.prototype과 Object.prototype을 상속받은 것이다. 그림으로 나타내면 다음과 같다.
위 코드에서 apple.hasOwnProperty라 함은 먼저 apple 객체->Fruit.Prototype->Object.prototype 이 순으로 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입에 해당 프로퍼티가 있는지 검색하여 Object.prototype의 hasOwnProperty 메서드를 사용한 것이다.
이러한 프로토타입 체인에도 당연히 종점은 존재한다. Object.prototype이 프로토타입 체인의 종점이다. Object.prototype의 [[Prototype]] 내부 슬롯의 값은 null이다.
또한 프로토타입은 아래 코드와 같이 인위적으로 교체할 수 있다. 교체할 경우 constructor 프로퍼티로 기존의 생성자 함수와의 연결했던 부분이 끊어지며 constructor 프로퍼티에 새로운 생성자 함수를 할당해서 다른 생성자 함수와 연결 또한 가능하다. 프로토타입 교체는 이정도로 간략하게 마무리하겠다.
// 생성자 함수에 의한 프로토타입 교체
Fruit.prototype = {
constructor : Person, // Person 생성자 함수가 정의되었다고 가정
newFunc() {
console.log("Change prototype");
}
}
console.log(Fruit.prototype.constructor === Fruit); // false
만약 apple 인스턴스에서 다음과 같이 메서드를 정의했다고 하자.
apple.itsPrice = function() {
console.log(`${this.name}'s price is only 1$`);
}
itsPrice 메서드는 Fruit.prototype의 메서드로 이미 정의되어있다. 하지만 apple 인스턴스에서 itsPrice 메서드를 정의함으로써 프로토타입 메서드 itsPrice를 오버라이딩하였고 프로토타입 메서드 itsPrice는 가려진다. 이런 현상을 프로퍼티 섀도잉이라 한다.
이항 연산자로서 좌변에 객체를 가리키는 식별자를, 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받는다. 만일 우변의 피연산자 함수가 아닌 경우 TypeError를 반환한다. 즉, 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인상에 존재하면 true이다.
객체 instanceof 생성자함수
function Fruit(name) {
this.name = name;
}
const apple = new Fruit("apple");
console.log(apple instanceof Fruit); // true