프로토타입 상속

등록일: 2014. 09. 16

퍼펙트 자바스크립트: 한 권으로 끝내는 모던 자바스크립트 프로그래밍

  • 이노우에 세이이치로, 츠치에 타쿠로, 하마베 쇼타 지음
  • 황선유 옮김
  • 740쪽
  • 32,000원
  • 2013년 02월 14일

이 절에서는 프로토타입 상속을 설명하지만 프로토타입 상속의 내부적인 동작 방식은 의외로 복잡합니다. 단순히 프로토타입 상속을 사용하고 싶기만 한 사람에게는 오히려 혼란을 일으킬 만한 위험이 있습니다. 그렇기 때문에 처음에는 형식만 설명합니다. 예제 5.9의 클래스 정의와 비슷한 것을 프로토타입 상속을 사용해 바꾼 것이 예제 5.11입니다.

예제 5.11 프로토타입 상속을 사용한 클래스 정의

// 클래스 정의에 해당
function MyClass(x, y) {
    this.x = x;
    this.y = y;
}

MyClass.prototype.show = function() {
                             print(this.x, this.y);
                         }
// 예제 5.11의 생성자 호출(인스턴스 생성)
js> var obj = new MyClass(3, 2);

// 메서드 호출
js> obj.show();
3 2

예제 5.9와 예제 5.11의 차이는 메서드 정의가 인스턴스 객체의 직접 프로퍼티냐 그렇지 않으냐입니다. 예제 5.11은 show 메서드가 obj 객체의 직접 프로퍼티가 아님에도 메서드를 호출할 수 있습니다. 겉만 보면 다른 객체(MyClass.prototype 객체)의 프로퍼티를 계승합니다(상속하고 있습니다). 이것이 프로토타입 상속의 형식적인 이해입니다.

자바스크립트는 값을 갖는 프로퍼티와 함수를 갖는 프로퍼티를 특별히 구별하지 않으므로 메서드 이외에도 프로토타입 상속을 할 수 있습니다. 단지 실제 사용을 고려하면 프로토타입 상속의 대상은 대체로 메서드입니다. 이 때문에 생성자 이름을 클래스 이름으로 바꿔도 지장은 없으므로 형식적으로 프로토타입 상속을 다음과 같이 기억해도 무방합니다.

// 프로토타입 상속의 형식적인 이해
클래스명.prototype.메서드명 = function ( 메서드 인자 ) { 메서드 본체 }

5-16-1 프로토타입 체인

프로토타입 상속은 프로토타입 체인이라고 하는 기능을 사용합니다. 프로토타입 체인의 전제는 다음의 두 가지입니다.

  • 모든 함수(객체)는 prototype이라는 이름의 프로퍼티를 갖는다(prototype 프로퍼티를 참조하는 객체를 prototype 객체라고 부르기로 하겠습니다)
  • 모든 객체는 객체 생성에 사용한 생성자(함수 객체)의 prototype 객체로 연결되는 (숨은) 링크를 갖는다

ECMAScript 명세에서는 prototype 프로퍼티를 명시적인 프로토타입 프로퍼티(explicit prototype property), 숨은 링크를 암묵적인 프로토타입 링크(implicit prototype link)라고 부릅니다. 이 책에서는 전자를 ‘prototype 참조’, 후자를 ‘암묵적인 링크’로 부르겠습니다.

이 전제를 이용하면 프로토타입 체인의 동작 방식은 다음과 같이 설명할 수 있습니다.

객체의 프로퍼티를 읽을 때(메서드 호출도 포함)는 다음과 같은 순서로 프로퍼티를 찾습니다.

  1. 객체 자신의 프로퍼티
  2. 암묵적인 링크의 참조 객체(=생성자의 prototype 객체)의 프로퍼티
  3. 2의 객체의 암묵적인 링크에 대한 참조 객체의 프로퍼티
  4. 3의 동작을 탐색이 끝날 때까지 계속한다(탐색의 끝은 Object.prototype 객체)

프로토타입 체인의 용어를 잠시 무시하면 요점은 암묵적인 링크의 프로퍼티의 상속입니다. 암묵적인 링크의 참조할 객체는 생성자의 prototype 객체이므로 앞 절의 ‘클래스명.prototype.메서드명’의 상속에 이야기가 연결됩니다. 객체 리터럴로 생성한 객체의 암묵적인 링크는 Object.prototype을 참조합니다.

객체의 프로퍼티 쓰기에서는 다음과 같은 순서로 프로퍼티를 찾습니다. 즉, 프로퍼티 쓰기는 상속 동작을 하지 않습니다.

  1. 객체 자신의 프로퍼티

읽기와 쓰기의 상속 동작 방식이 다른 것에 주의해 주십시오. 이 같은 비대칭성은 어떤 의미에서 당연한 동작 방식입니다. 프로토타입 체인에 의해 모든 객체는 최종적으로 Object.prototype 객체로의 암묵적인 링크를 가집니다. 만약 프로퍼티 덮어쓰기가 체인의 상위로 전파한다고 한다면 어떤 객체가 toString 메서드를 덮어쓰는 것만으로 모든 객체에 영향을 주게 됩니다. 이런 일이 일어나면 손을 쓸 수가 없습니다.

한편, 읽기는 상속하므로 어떤 암묵적인 링크의 toString 메서드를 덮어쓰면 바닥부터 프로토타입 상속한 객체는 그것이 적용된 새로운 구현을 사용할 수 있습니다. 이것은 구현의 상속 또는 특징의 상속이라는 객체지향 기법의 적절한 활용입니다. 약간 용어가 혼란될 수 있지만 ‘암묵적인 링크’의 참조할 객체를 프로토타입 객체라고 합니다(컬럼 참고). 이 용어를 받아들이면 프로토타입 상속을 설명하기가 매우 간단해집니다. 즉, ‘프로퍼티를 읽을 때 프로토타입 객체의 프로퍼티를 상속한다’라는 한마디로 설명이 끝납니다.

그림 5.6은 프로토타입 체인의 동작 방식을 보여줍니다. 변수와 참조할 객체를 나눠 쓰므로 주의해 주십시오. 객체 자체는 이름이 없다는 것을 지금 한번 확인해 주십시오. 이를 이해하기 어렵다면 ‘5-2-3 객체와 참조에 관련된 용어의 정리’에 나온 설명을 참조합니다.

그림 5.6 프로토타입 체인의 동작 방식

5-16-2 프로토타입 체인의 구체적인 예

프로토타입 체인의 구체적인 예와 내부 동작 방식을 설명하겠습니다. 우선 실습 5.4를 봐 주십시오.

js> function MyClass() { this.x = 'x in MyClass'; }

js> var obj = new MyClass();  // MyClass 생성자로 객체 생성
js> print(obj.x);             // 객체 obj의 프로퍼티 x에 접근
x in MyClass

js> print(obj.z);             // 객체 obj에 프로퍼티 z는 없다
undefined

// 함수 객체는 암묵적으로 prototype 프로퍼티를 가진다
js> MyClass.prototype.z = 'z in MyClass.prototype';
                             // 생성자의 prototype 객체에 프로퍼티 z를 추가

js> print(obj.z);            // obj.z는 생성자의 prototype 객체의 프로퍼티에 접근
z in MyClass.prototype

실습 5.4 프로토타입 체인의 구체적인 예(프로퍼티 읽기)

객체 obj의 프로퍼티를 읽을 때 처음에 자기 자신의 프로퍼티를 찾습니다. 이를 찾지 못하는 경우 다음에 MyClass 객체의 prototype 객체에 포함된 프로퍼티를 찾습니다. 이러한 동작 방식이 프로토타입 체인의 기본입니다. 이것에 의해 MyClass 생성자에서 생성한 각 객체는 MyClass.prototype 객체의 프로퍼티를 공유합니다.

이러한 프로퍼티 공유를 객체지향 용어로 바꿔말하면 상속입니다. 상속에 의해 같은 특징을 보이는 객체를 생성할 수 있습니다. 위 코드에서 MyClass.prototype의 변경사항이 이미 생성이 끝난 객체에도 반영된 것에 주의해 주십시오.

프로퍼티 쓰기와 제거는 프로토타입 체인을 따라가지 않습니다. 실습 5.5, 실습 5.6, 그림 5.7을 보겠습니다.

js> function MyClass() { this.x = 'x in MyClass'; }
js> MyClass.prototype.y = 'y in MyClass.prototype';

js> var obj = new MyClass();   // MyClass 생성자로 객체 생성
js> print(obj.y);              // 프로토타입 체인에서 프로퍼티 y 읽기
y in MyClass.prototype

js> obj.y = 'override';        // 객체 obj에 직접 프로퍼티 y를 추가
js> print(obj.y);              // 직접 프로퍼티를 읽음
'override'

js> var obj2 = new MyClass();
js> print(obj2.y);             // 다른 객체에서 보이는 프로퍼티 y는 그대로임
y in MyClass.prototype

실습 5.5 프로토타입 체인의 구체적인 예(프로퍼티 쓰기)

js> delete obj.y;              // 프로퍼티 y를 제거

js> print(obj.y);              // 직접 프로퍼티가 없어지면 프로토타입 체인을 따름
y in MyClass.prototype

js> delete obj.y;              // delete 연산의 평가값은 true이지만...
true
js> print(obj.y);              // 프로토타입 체인 앞의 프로퍼티는 delete할 수 없음
y in MyClass.prototype

실습 5.6 프로토타입 체인의 구체적인 예(프로퍼티 제거) (실습 5.5에 이어)

그림 5.7 실습 5.5와 실습 5.6의 동작 방식

5-16-3 프로토타입 상속과 클래스

실습 5.4에서 MyClass.prototype 객체의 프로퍼티를 찾지 못하면 다시 프로토타입 체인의 탐색을 계속합니다.

MyClass.prototype 객체를 생성한 생성자의 protorype 객체의 프로퍼티를 찾습니다. 기본적으로 MyClass.prototype 객체의 생성자는 Object 객체이기 때문에 Object.prototype 객체의 프로퍼티를 찾습니다. Object.prototype 객체에 새로운 프로퍼티를 추가하면 동작 방식을 확인할 수 있습니다. 실제 코드에서 Object.prototype을 변경하는 것은 영향의 범위가 크기 때문에 권장하지 않습니다. 원래부터 Object.prototype 객체에 있는 프로퍼티 중 하나인 toString 메서드를 호출하는 것으로 동작 방식을 확인해 보겠습니다(실습 5.7). toString 프로퍼티를 갖는 것은 Object 객체가 아닌 Object.prototype 객체라는 점에 유의해 주십시오. 프로퍼티를 실제로 가진 객체에 대해서는 hasOwnProperty 메서드로 확인할 수 있습니다.

js> obj.toString();                               // 객체 obj에 대해 toString 메서드를
[object Object]                                   // 호출할 수 있음을 확인

js> obj.hasOwnProperty('toString');               // 객체 obj에 toString 메서드는
false                                             // 존재하지 않음
js> Object.prototype.hasOwnProperty('toString');  // Object.prototype객체에
true                                              // toString 메서드가 존재

js> Object.hasOwnProperty('toString');            // Object에는 toString 메서드가
false                                             // 존재하지 않는 것에 주의

실습 5.7 Object.prototype으로의 암묵적인 링크가 있음을 확인(실습 5.4에 이어서)

프로토타입 상속에서 자바나 C++ 등의 클래스 기반 언어의 타입 계층과 비슷한 동작 방식을 구현할 수 있습니다.

5-16-4 프로토타입 체인과 관련해서 자주 볼 수 있는 착각과 proto 프로퍼티

프로토타입 체인은 다음과 같이 착각하기 쉽습니다.

  • 자기 자신의 프로퍼티를 살펴본 후 생성자의 프로퍼티를 찾는다(실습 5.4로 말하면 obj.prototype.y를 본다는 착각)
  • 자기 자신의 프로퍼티를 살펴본 후 객체의 prototype 객체 프로퍼티를 찾는다(실습 5.4로 말하면 MyClass.y를 본다는 착각)

프로토타입 체인이 가는 도착점은 어디까지나 ‘암묵적인 링크’입니다. 일부 자바스크립트 구현에는 암묵적인 링크를 할 객체를 참조하는 proto 프로퍼티가 있습니다.

ECMAScript 사양에는 없으므로 proto 프로퍼티의 존재는 구현에 의존합니다.

5-16-5 프로토타입 객체

객체의 암묵적인 링크(proto 프로퍼티)가 참조하는 객체를 프로토타입 객체라고 합니다. 이러한 호칭이 혼동하기 쉽다는 것을 다음 코드를 보면서 설명하겠습니다.

function MyClass() {}
var obj = new MyClass();

MyClass.prototype과 obj.proto는 같은 객체를 참조합니다. 이것이 객체 obj의 프로토타입 객체입니다. 이를 혼동할 수 있지만 MyClass.prototype의 참조 객체는 MyClass의 프로토타입 객체가 아닙니다(MyClass 객체의 프로토타입 객체는 무엇인지 아시겠나요?). 답은 Function.prototype이 참조하는 객체입니다. 자세한 사항은 ‘6-6-1 Function 클래스의 상속’을 참고해 주십시오.

‘5-2-3 객체와 참조에 관련된 용어의 정리’에서 변수 obj가 참조하는 객체를 편의상 ‘객체 obj’라고 부르기로 정했습니다. 본 절에서는 굳이 ‘MyClass.prototype 객체’라는 호칭을 사용하지 않았습니다. 이렇게 쓰면 혼란을 더 가중시킬 것이라고 생각하기 때문입니다. 이름이 없는 객체를 (가끔) 그것을 참조하는 변수를 사용해 이름이 있는 것처럼 표기해서 혼란을 초래하는 것이 좋은 예입니다.

5-16-6 프로토타입 객체와 ECMAScript 5

프로토타입 체인의 동작 방식을 알기 어려운 원인 중 하나가 암묵적인 링크의 존재입니다. 앞에서 소개한 proto 프로퍼티는 독자 확장이기 때문에 사실상 객체에서 프로토타입 객체를 따르는 공식적인 수단은 없었습니다. 이것이 바로 암묵적인 링크라고 불리는 이유입니다.

이 상황은 ECMAScript 5에서 바뀌었습니다. ECMAScript 5에는 getPrototypeOf 메서드가 있습니다. 이것은 ‘암묵적인 링크’가 가리키는 객체를 반환합니다. 즉, 독자 확장인 proto 프로퍼티와 같은 동작을 하는 메서드가 공식 규격에 들어갔습니다. 객체에서 프로토타입 객체를 구하는 구체적인 방법을 예제 5.12에 소개하겠습니다.

예제 5.12 프로토타입 객체를 구하는 세 가지 방법

// 전제
function MyClass() {}
var Proto = MyClass.prototype;
var obj = new MyClass();     // 객체 obj의 프로토타입 객체는 객체 Proto

// 인스턴스 객체에서 취득(ECMAScript 5 표준)
var Proto = Object.getPrototypeOf(obj);

// 인스턴스 객체에서 취득(독자 확장인 __proto__ 프로퍼티 이용)
var Proto = obj.__proto__;

// 인스턴스 객체에서 생성자를 통해 취득(항상 쓸 수 있다는 보장은 없음)
var Proto = obj.constructor.prototype;

독자 확장이지만 이후의 설명에서 proto 프로퍼티를 설명할 때 사용합니다. 다른 것보다 직관적으로 이해하기 쉽기 때문입니다. 표준 준수를 고집하고 싶은 분은 proto를 사용한 부분을 Object.getPrototypeOf의 코드로 바꾸면 됩니다.