본문으로 바로가기

프로토타입 체인

자바스크립트는 Function() 인스턴스에 자동으로 prototype 이라는 속성을 만듭니다.

구체적으로 말하자면 prototype 속성은 new 키워드와 생성자 함수를 같이 사용해서 만든 객체에 연결됩니다.

인스턴스들은 생성자 함수의 prototype 속성을 통해 공통의 메소드와 속성을 공유하고 상속합니다.

중요한 사실은 이러한 공유가 속성을 검색하는 동안 일어난다는 것입니다.

즉, 어떤 객체의 속성을 찾거나 접근하면 자바스크립트는 그때마다 해당 객체는 물론 프로토타입 체인에서도 그 속성을 찾는다는 것입니다.




함수는 모두 prototype 객체를 포함하고 있다.

심지어 생성자 함수로 사용할 생각이 전혀 없는 함수도 prototype 객체를 자동으로 포함합니다.

다음은 Array() 생성자를 이용해 배열을 하나 만든 후 배열의 join() 메소드를 호출하는 코드입니다.

javascript
var myArr = new Array('foo', 'bar');

console.log(myArr.join()); // foo,bar 가 기록된다.

join() 메소드는 myArr 객체 인스턴스에 정의되어 있지 않은 속성입니다.

하지만 사용자는 join() 메소드가 마치 원래 있던 것처럼 사용할 수 있었습니다.

분명히 이 메소드는 어디엔가 정의되어 있을 것입니다.

하지만 어디일까? 이 메소드는 Array() 생성자의 prototype 속성에 속성으로서 정의되어 있습니다.

배열 객체 인스턴스에서는 join() 메소드가 없기 때문에 자바스크립트는 프로토타입 체인에서 join() 이라는 메소드가 있는지 검색합니다.


왜 이런 식으로 동작하도록 만들었을까?

사실 이는 효율성과 재사용성에 대한 문제입니다.

배열 생성자 함수가 만드는 배열 인스턴스마다 언제나 똑같은 방식으로 동작하는 join() 메소드를 굳이 일일이 추가할 필요가 있을까?

모든 배열 인스턴스마다 함수를 새로 만드는 것보다 하나의 join() 메소드를 모든 배열에서 가져다 쓰는 편이 더 합리적일 것입니다.


자바스크립트에서는 prototype 속성, 프로토타입 결합(prototype linkage), 프로토타입 체인 검색을 통해 이러한 효율성을 이루고 있습니다.



prototype 속성은 왜 중요한가?!

네이티브 생성자 함수(예: Object(), Array(), Function() 등)는 prototype 속성을 사용해 생성자 인스턴스가 메소드와 속성을 상속받도록 하고 있기 때문입니다.

자바스크립트는 이러한 메커니즘을 사용해 객체 인스턴스가 생성자 함수의 prototype 속성을 상속받을 수 있도록 지원해 주고 있습니다.

따라서, 자바스크립트를 더 잘 이해하려면 자바스크립트가 prototype 객체를 어떻게 사용하는지 잘 알아야 할 것입니다.

사용자 정의 생성자 함수를 만들 때 자바스크립트 네이티브 객체와 동일한 방식을 통해 프로토타입 상속을 구현할 수 있습니다.

그러나 상속이 이루어지는 방식에 대한 이해가 반드시 수반되어야 합니다.

프로토타입 상속을 싫어하거나 다른 상속 패턴을 더 선호할 수도 있겠지만 현실적으로 생각해 볼 때 아마도 누군가 프로토타입 상속을 사용해 구현해 놓은 코드를 수정하거나 조작해야 할 일이 생길 것입니다.

이때 프로토타입 상속이 어떻게 동작하는지 알아야 다른 개발자가 만든 생성자 함수의 기능을 그대로 복제할 수 있을 것입니다.

프로토타입 상속을 사용하면 동일한 메소드를 공유하는 여러 개의 효율적인 객체 인스턴스를 만들 수 있습니다.

앞서 말한 것과 같이 Array() 생성자의 인스턴스인 배열 객체는 굳이 인스턴스마다 join() 메소드를 추가해주지 않아도 됩니다.

모든 배열 인스턴스의 프로토타입 체인에는 join() 메소드가 저장되어 있으므로 모든 배열 인스턴스는 동일한 join() 메소드르 사용할수 있습니다.



모든 Function() 인스턴스에는 prototype 속성이 있다.

자바스크립트에서 함수는 Function() 생성자를 직접 호출하여 만들었든 리터럴 표기법을 사용해 만들었든 예외없이 모드 Function() 생성자로부터 만들어집니다.

함수 인스턴스를 만들면 인스턴스에는 항상 prototype 속성이 추가됩니다.

이때 prototype 속성은 그 자체로는 빈 객체와 같습니다.


다음 코드에 myFn 이라는 함수를 정의한 후, 이 함수의 prototype 속성에 접근해 보도록 해봅니다.

javascript
var myFn = function () {

};
console.log(myFn.prototype); // Object() 가 기록된다.
console.log(typeof myFn.prototype); // 'object' 가 기록된다.

Function() 생성자를 사용할 때문 언제나 자동으로 설정되는 prototype 속성은 중요한 속성이므로 잘 이해하도록 해야합니다.

비록 위 코드에서는 생성자로 사용한 사용저 정의 함수가 하나밖에 없지만, Function() 생성자는 모든 함수 인스턴스에 prototype 속성을 부여합니다.



prototype 속성은 Object() 객체다.

프로토타입에 대한 내용은 조금 어렵습니다.

엄밀히 말해 prototype 은 Function() 인스턴스를 만들 때 자바스크립트가 인스턴스에 부여하는 이름이 'prototype' 이고 기본값이 빈 객체인 속성일 뿐입니다.

만약 이를 사용자가 직접 코드로 만든다면 다음과 같이 작성할 수 있을 것입니다.

javascript
var myFunc = function () {

};

myFunc.prototype = {}; // 시용자가 직접 prototype 속성을 추가하고 속성값으로 빈 객체를 할당
console.log(myFunc.prototype); // 빈 객체인 Object {} 가 기록된다.


사실 prototype 속성의 값으로는 어떤 복합 객체(예: 배열등..)든 사용할 수 있습니다.

prototype 에 원시값을 설정하려 하면 자바스크립트에서는 해당 코드를 무시하게 된다.



생성자 함수를 통해 만든 인스턴스는 생성자 함수의 prototype 속성과 연결되어 있다.

생성자 함수의 prototype 속성은 그 자체만 놓고 보면 객체일 뿐이지만 프로토타입 체인을 통해 인스턴스와 연결되는 특이한 속성입니다.

다시말해, new 키워드로 생성자 함수를 사용해 객체를 만들면 생성자 함수의 prototype 속성과 새롭게 만들어진 객체 인스턴스 사이에는 일종의 숨겨진 연결고리가 생깁니다.

일부 브라우저에서는 이 연결고리가 인스턴스의 __proto__속성으로 나타나기도 합니다.

자바스크립트는 생성자 함수를 사용해 인스턴스를 만들 때 인스턴스 객체와 생성자 함수를 자동으로 연결해 두며, 이러한 연결 덕분에 프로토타입 체인이 형성됩니다.


다음은 네이티브 Array() 생성자의 prototype 에 속성을 하나 추가한 후 배열 인스턴스의 __proto__ 속성을 사용해 추가한 속성에 접근하는 코드입니다.

javascript
// 해당 코드는 __proto__ 를 구현해 놓은 브라우저(FF, Chrome, 사파리, 안드로이드)에서만 동작한다.
Array.prototype.foo = 'foo';

var myArr = new Array();
console.log(myArr.__proto__.foo);

// myArr.__proto__ == Array.prototype 이므로 foo 가 기록될 것이다.

사실 __proto__ 속성은 ECMA 공식 표준이 아니기 때문에 이보다 다른 방법을 사용할 수 있습니다.

객체 인스턴스에서 constructor 속성을 사용하면 생성자를 구할 수 있고 이를 통해 인스턴스가 상속받은 프로토타입 객체를 구할 수 있습니다.


다음 코드를 살펴봅니다.

javascript
Array.prototype.foo = 'foo'; // Array() 인스턴스는 모두 foo 속성을 상속받게 된다.

var myArray = new Array();
// .constructor.prototype 을 사용하면 조금 복잡하지만 foo 속성을 구할 수 있다.
console.log(myArray.constructor.prototype.foo); // foo 가 기록된다.

// 물론 프로토타입 체인을 사용해도 된다.
console.log(myArray.foo); // 프로토타입 체인을 사용한 Array.prototype.foo 를 검색하게 된다.

이 코드에서 foo 속성은 prototype 객체에서 찾을 수 있었습니다.

이는 Array() 인스턴스와 Array() 생성자의 프로토타입 객체(==Array.prototype) 간에 연결고리가 있었기 때문에 가능한 것이었습니다.

간단히 말해서 myArray.__proto__(또는 myArray.constructor.prototype) 은 Array.prototype 을 참조합니다.



프로토타입 체인의 끝은 Object.prototype 이다.

prototype 속성은 객체 이기 때문에 프로토타입 체인 또는 프로토타입 검색의 종점은 Object.prototype 이다.

javascript
var meArray = [];
console.log(meArray.foo); // undefined 가 기록된다.

/*
meArray.foo, Array.prototype.foo, Object.prototype.foo 에서 foo 를 찾지 못했기 때문에
foo 는 undefined 를 기록하게 된다.
*/

위 코드에서 meArray 라는 빈 배열을 만든 후 프로토타입 체인을 검색하도록 meArray 에 정의되지 않은 속성에 접근해 보았습니다.

자바스크립트는 먼저 meArray 객체에서 foo 속성을 찾아보지만, 찾지 못하기 때문에 Array.prototype 에서 다시 검색을 시도할 것입니다.

하지만 여기서도 찾을 수 없으므로 마지막 종착지인 Object.prototype 에서 foo 속성을 찾아볼 것입니다.

하지만 검색의 끝 지점인 여기서에서 찾을 수 없으므로 결국 foo 속성은 undefined 가 될 것입니다.



프로토타입 체인은 Object.prototype 에서 끝난다.

foo 속성이 있는지 가장 마지막에 찾아본 곳은 Object.prototype 이었다.

Object.prototype 에 속성을 추가하면 추가한 속성이 for in 반복문에 나타난다.



프로토타입 체인은 체인에서 제일 먼저 찾은 속성을 반환한다.

스코프 체인과 마찬가지로 프로토타입 체인은 체인을 검색하다가 가장 먼저 발견한 값을 사용합니다.

바로 앞에서 사용했던 코드를 조금 수정하여 Object.prototypeArray.prototype 객체에 똑같은 속성을 추가한 후 배열 인스턴스에서 그 값에 접근해 보도록 하겠습니다.

Array.prototype 객체의 값이 반환될 것입니다.

javascript
Object.prototype.foo = 'object-foo';
Array.prototype.foo = 'array-foo';

var arr = [];
console.log(arr.foo); // Array.prototype.foo 에서 찾은 'array-foo' 가 기록된다.

arr.foo = 'bar';
console.log(arr.foo); // arr.foo 에서 찾은 'bar' 가 기록된다.

이 코드에서 Array.prototype.foo 의 foo 값은 Object.prototype.foo 의 foo 값을 가리게 됩니다.

프로토타입에서 속성을 검색할 때는 가장 먼저 발견한 속성을 사용하고 검색을 종료합니다.

체인에 똑같은 이름의 속성이 몇 개나 더 있었는지는 상관없습니다.



prototype 속성을 새 객체로 대체하면 기본 constructor 속성이 삭제된다.

prototype 속성의 기본값은 다른 값으로 대체할 수 있습니다.

하지만 prototype 속성을 바꾸면 원래의 prototype 객체에서 볼 수 있었던 기본 constructor 속성도 사라지게 됩니다.

다음은 Foo 생성자 함수를 만들고 Foo 의 prototype 속성을 빈 객체로 대체한 후 인스턴스의 constructor 속성이 사라졌는지 확인해 보는 코드입니다.

이제 이 코드에서는 constructor 속성은 Object() 생성자를 참조하게 될 것입니다.

javascript
var Foo = function Foo() {

};

Foo.prototype = {}; // 빈 객체로 prototype 속성을 대체한다.

var fooInstance = new Foo();
console.log(fooInstance.constructor == Foo); // false 가 기록된다.
// 참조가 망가졌다. 즉, 연결고리가 끊어진 것이다.

console.log(fooInstance.constructor); // Foo 생성자 함수 가 아닌 Object() 가 기록된다.


// prototype 값을 대체하지 않은 경우와 비교해 본다.
var Bar = function Bar() {

};
var barInstance = new Bar();

console.log(barInstance.constructor == Bar); // true 가 기록된다.
console.log(barInstance.constructor); // Bar 생성자 함수가 기록된다.

만약 자바스크립트가 설정한 기본 prototype 속성을 대체할 생각이라면 즉, 자바스크립트 객체지향 패턴에서 종종 사용되는 방식등을 사용할 경우에는 생성자 함수를 참조하는 constructor 속성을 원래대로 복원해주어야 합니다.


다음은 앞의 코드를 조금 수정하여 constructor 속성이 원래의 생성자 함수를 올바르게 참조하도록 해봅니다.

javascript
var Foo = function Foo() {

};
Foo.prototype = { constructor : Foo };

var fooInstance = new Foo();

console.log(fooInstance.constructor == Foo); // true 가 기록된다.
console.log(fooInstance.constructor); // Foo 생성자 함수가 기록된다.



프로토타입에서 상속한 속성은 가장 최근의 값을 사용한다.

인스턴스가 프로토타입에서 상속한 속성은 그 속성이 어떻게 만들어지고, 변경되고, 추가되었든 상관없이 항상 가장 최근의 값을 사용합니다.

다음 코드는 Foo 생성자를 만들고 x 라는 속성을 prototype 에 추가한 후 Foo() 생성자의 인스턴스를 fooInstance 라는 이름으로 만들었습니다.

여기서 한번 x 의 값을 기록한 후 생성자의 prototype 속성에 포함된 x 의 값을 수정한 후 다시 한번 x 의 값을 기록했습니다.

이를 통해 우리는 인스턴스가 prototype 객체에서 가져오는 값은 가장 마지막 값이라는 사실을 알 수 있을 것입니다.

javascript
var Foo = function Foo() {

};

Foo.prototype.x = 10;

var FooInstance = new Foo();

console.log(FooInstance.x); // 10 이 기록된다.

Foo.prototype.x = 20;

console.log(FooInstance.x); // 20 이 기록된다. FooInstance 도 갱신된 것이다.

이러한 현상은 기본 prototype 객체를 사용했든 또는 prototype 객체를 새로운 객체로 대체했든 상관없이 똑같이 볼 수 있습니다.

다음 코드에서는 기본 prototype 객체 대신에 새로운 객체를 사용해 이 같은 사실을 증명해 보도록 합니다.

javascript
var Foo = function Foo() {

};

Foo.prototype = { x: 10};

var FooInstance = new Foo();

console.log(FooInstance.x); // 10 이 기록된다.

Foo.prototype.x = 20;

console.log(FooInstance.x); // 20 이 기록된다. FooInstance 도 갱신된 것이다.



prototype 속성을 새 객체로 대체하면 이전에 만든 인스턴스는 갱신되지 않는다.

지금까지 살펴본 바에 따르면 아마도 prototype 속성을 언제든 완전히 대체할 수 있고 그렇게 하면 모든 인스턴스가 갱신될 것이라고 생각할 수도 있습니다.

하지만 이는 잘못된 생각입니다.

인스턴스를 만들면 인스턴스를 만들 때의 prototype 과 인스턴스가 서로 묶여 버리기 때문에 prototype 속성에 새 객체를 설정하면 이미 만들어진 인스턴스와 새로운 prototype 간의 연결고리가 끊어져버리게 됩니다.

하지만 앞서 살펴본 방법을 사용하면 인스턴스를 만들었던 시점의 prototype 객체를 찾아서 값을 수정하거나 추가할 수 있으며 이를 통해 이 prototype 과 연결괸 인스턴스를 갱신할 수 있습니다.

javascript
var Foo = function Foo() {

};

Foo.prototype.x = 10;

var FooInstance = new Foo();
console.log(FooInstance.x);  // 예상한 대로 10 이 기록된다.

// prototype 객체를 새로 만든 Object() 객체로 대체/정의해 보도록 한다.
Foo.prototype = {x: 20};

console.log(FooInstance.x); // 10 이 기록된다.
/**
 * 위에서 prototype 을 새로운 객체로 갱신했으니까 20 이 되어야 하지 않을까? 라고 생각할 수 있다.
 * FooInstance 는 여전이 처음 인스턴스로 만들어진 시점의 prototype 객체를 참조하고 있다.
 **/

// Foo() 의 인스턴스를 새로 만들어 본다.
var NewFooInstance = new Foo();

// 새로 만든 인스턴스는 새로운 prototype 객체인 { x : 20 } 와 묶여있게 된다.
console.log(NewFooInstance.x); // 20 이 기록된다.

여기서 알 수 있는 사실은 인스턴스를 만든 뒤에는 객체의 prototype 속성을 새 객체로 대체하면 안된다는 것입니다.

만약 새 객체로 대체해버리면 같은 생성자에서 만든 인스턴스라 해도 서로 다른 prototype 객체를 참조하게 될 것입니다.



사용자 정의 생성자도 네이티브 생성자처럼 프로토타입을 상속할 수 있다.

지금까지 자바스크립트에서 prototype 속성을 사용해 상속하는 법(예: Array.prototype)을 살펴보았습니다.

같은 패턴을 네이티브가 아닌 사용자가 직접 만든 생성자에도 적용할 수 있습니다.

다음은 Person 사용자 생성자함수를 가지고 객체를 생성하여 자바스크립트 상속 패턴을 흉내내는 코드입니다.

javascript
var Person = function () {

};

// 모든 Person 인스턴스는 legs, arms, countLimbs 속성을 상속하도록 정의한다.
Person.prototype.legs = 2;
Person.prototype.arms = 2;
Person.prototype.countLimbs = function () {
    return this.legs + this.arms;
};

var chuck = new Person();

console.log(chuck.countLimbs()); // 4 가 기록된다.

위의 코드에서 Person() 생성자 함수를 만든 후 Person() 의 prototype 속성에 몇 개의 속성을 추가하여 모든 인스턴스가 상속받도록 정의했습니다.

이 코드는 자바스크립트가 네이티브 객체를 상속할 대 사용했던 것과 똑같은 방법으로 프로토타입 체인을 사용한 것입니다.

전달된 매개변수가 없을 때 프로토타입에서 속성을 상속받은 생성자 함수를 만들어보면 이를 조금 더 잘 이해할 수 있을 것입니다.

다음 코드에서 Person() 생성자는 전달된 매개변수가 있으면 이를 사용해 인스턴스 속성을 추가하지만 전달된 값이 아예 없거나 한 개만 있으면 프로토타입에서 상속받은 값을 사용합니다.

인스턴스 속성이 있으면 상속된 속성이 사용되지 않습니다.

따라서 어느 경우에든 속성을 문제없이 사용할 수 있을 것입니다.

javascript
var Person = function (legs, arms) {

    // 프로토타입에서 상속받은 값을 가린다.
    if (legs !== undefined) {
        this.legs = legs;
    }
    if (arms !== undefined) {
        this.arms = arms;
    }

};

Person.prototype.legs = 2;
Person.prototype.arms = 2;
Person.prototype.countLimbs = function () {
    return this.legs + this.arms;
};

var chuck = new Person(4,4);
console.log(chuck.countLimbs()); // 8 이 기록된다.

// 매개변수를 전달하지 않을 경우
var chuck2 = new Person();
console.log(chuck2.countLimbs()); // 4 가 기록된다.
// 전달된 매개변수가 없기 때문에 Person 에는 속성을 가지고 있지 않다.
// 하지만 프로토타입 체인을 통해 검색한 결과를 통해 4 를 기록하게 되는 것이다.



상속 체인 만들기

프로토타입 상속은 전통적인 객체 지향 프로그래밍 언어에서 볼 수 있던 상속 패턴을 흉내내기 위해 만들어진 것입니다.

자바스크립트에서 인스턴스란 간단히 말해 다른 객체의 속성에 접근할 수 있는 객체입니다.

이를 위해 먼저 상속받고자 하는 부모 객체의 인스턴스를 만든 후 생성자의 prototype 에 할당하면 부모 객체를 상속받을 수 있습니다.

prototype 속성에 부모 객체의 인스턴스를 할당하고 나면 부모 객체 생성자의 prototype 과 상속받는 객체 사이에는 연결고리(__proto)가 생기게 됩니다.


다음의 코드를 살펴봅니다.

javascript
var Person = function () {
    this.bar = 'bar';
};

Person.prototype.foo = 'foo';

var Chef = function () {
    this.goo = 'goo';
};
Chef.prototype = new Person(); // Person() 객체의 인스턴스를 할당

var cody = new Chef();

위 코드에서 Chef 객체(=cody) 는 Person 을 상속받습니다.

다시 말해, 어떤 속성을 Chef 객체에서 발견하지 못하면 그 다음에는 Person() 생성자 함수의 prototype 에서 속성을 찾는다는 뜻입니다.

이렇게 상속 관계를 만들기 위해 해야 할 일은 Chef.prototype 의 값으로 Person() 객체의 인스턴스를 할당 해주는 것이 전부입니다.

위 코드에서 한 일은 네이티브 객체에서 원래 사용하고 있던 시스템을 빌려온 것 뿐입니다.

prototype 속성에 있어서 원래 사용하던 Object() 값이나 Person() 값은 다른 점이 없습니다.

다시 말해, 객체의 상속된 속성에 접근하면 객체를 만든 생성자 함수의 prototype 속성에서 그 값을 찾을 것이라는 의미입니다.




Jaehee's WebClub