자바스크립트는 객체 지향 언어이며, 객체가 행위와 속성을 가진다는 개념을 object가 property를 가진다는 것으로 구현한다.
(이것이 지난 포스팅까지 다룬 개념이다)
이번 포스팅에서는 서로 다른 두 객체가 [[Prototype]]을 통해 어떻게 연결되는지에 대해 알아본다.
객체에는 [[Prototype]]이라는 내부 property가 있다.
이 [[Prototype]]은 수임객체를 참조한다.
개발자가 따로 처리하지 않는다면, 객체 생성 시점에 [[Prototype]]에 참조할 객체의 메모리 주소값이 할당되기 마련이다
거꾸로 말하면, 개발자가 따로 처리를 하는 경우 [[Prototype]]이 다른 객체를 참조하지 않게끔 만들 수도 있다는 것이다.
이때 참조되는 객체를 수임객체라고 하고, 참조하는 객체를 위임객체라고 한다.
'위임'과 '수임'이라는 용어를 쓰는 이유는 메소드 호출의 사례를 '클래스 지향'과 비교해보면 유추할 수 있다.
'클래스 지향'에서는 인스턴스의 생성도, 클래스의 상속도 모두 '복사'로 이루어진다.
따라서 인스턴스에서 특정 메소드를 호출하는 것은 복사된 메소드, 즉 자신의 메소드를 호출하는 것이다.
반면 자바스크립트에서는 위임객체의 own property에 찾는 메소드가 없으면 [[Prototype]]을 타고 올라가 수임객체의 메소드를 호출한다.
즉, 위임객체가 메소드의 작동을 수임객체에게 '위임'하는 것이다.
(호출의 주체는 위임객체이다. 이는 this바인딩에서 더욱 자세하게 다룰 것이다)
일반적으로 함수의 prototype이 참조되는 것이지, prototype이 곧 수임객체인 것은 아니다.
지난 포스팅에서 object의 property에 접근할 때, object의 [[Get]]을 호출한다고 했다.
찾는 property가 own property에 없다면, [[Get]]은 [[Prototype]]을 타고 올라가 property를 찾는다.
[[Prototype]] chain을 통해 수임객체의 property에 접근할 수 있으니 이를 이용해서 수임객체의 property를 바꾸면 되겠다고 생각할 수도 있다.
결론부터 말하자면 이는 지양해야 할 행위이며, 얼마나 복잡한 일인지 아래의 예제를 통해 살펴보자.
예제가 다소 장황하기 때문에 위임객체에서 수임객체의 property를 바꾸려고 하지 말자는 것만 기억하고 1-3파트로 넘어가도 무방하다.
첫번째로 woo property에 대해 살펴보자.
Object.prototype에 woo property를 writable:true로 설정했다.
obj에는 woo가 없으니 obj["woo"]는 Object.prototype.woo를 가리킬 것이고,
writable값이 true이니 Object.prototype.woo의 값이 변경될 것이라고 기대할 것이다.
하지만 Object.prototype.woo의 값은 변경되지 않고 obj에 woo property가 생기고 1이 할당되었다.
(누가 설계했냐)
두번째로 bun property에 대해 살펴보자.
Object.prototype에 bun property를 writable:false로 설정했다.
obj에는 bun이 없으니 obj["bun"]은 Object.prototype.bun을 가리킬 것이고,
writable값이 false이니 Object.prototype.bun의 값은 변경되지 않는다.
세번째로 tu property에 대해 살펴보자.
Object.prototype에 tu property를 set함수로 설정했다.
obj에는 tu가 없으니 obj["tu"]는 Object.prototype.tu를 가리킬 것이고,
set함수를 할당문 형식으로 호출했으니 정의된 set함수가 실행된다.
[[Prototype]] chain을 통해 수임객체의 property를 바꿀 수 있을거라 생각했지만 불가능했다.
이를 통해 property value의 할당 및 변경은 오직 own property차원에서 이루어져야 함을 알 수 있다.
(물론 writable이 true인 경우에만 가능하다)
첫 포스팅에서 이번 시리즈는 온전히 자바스크립트의 관점에서 '객체 지향' 개념을 서술할 것이라고 했다.
YDKJS를 읽어본 결과, 자바스크립트의 본질에 가까운 디자인 패턴은 '클래스 지향'이 아니라 '위임'이다.
그러나 개발자들 사이에서 자바스크립트로 '클래스 지향'을 흉내내는 것이 오랜 관습처럼 굳어져서인지 대부분의 책에서는 [[Prototype]] chain마저 '클래스 지향'의 관점에서 서술하고 있다.
오히려 '위임'에 대한 자료보다 이런 '클래스 지향' 관점의 자료가 압도적으로 많기에 일단 [[Prototype]] chain은 '클래스 지향'의 관점을 곁들여 설명하고자 한다.
하지만 자바스크립트의 주된 작동 방식이 '복사'가 아닌 '참조'라는 것을 항상 염두에 둬야 하며, 최대한 이를 반영해서 서술할 것이다.
위임 디자인 패턴의 개념에 대해서는 마지막에 아주 간략하게 언급만 하고 넘어가고자 한다.
(이번 시리즈의 목적은 어디까지나 자바스크립트의 이해이기 때문이며,
디자인 패턴에 대해서는 이후에 '함수형 프로그래밍'시리즈로 작성하고자 한다)
function Woobuntu() {};
이와 같이 개발자가 새로 정의한 함수에도 prototype이라는 property가 자동으로 생성된다.
이제 Array 함수를 new 키워드와 함께 사용해보자.
new 키워드와 함수를 함께 사용해 생성해낸 객체인 arr는 Array.prototype에 정의된 concat메소드를 쓸 수 있다.
보이는 것이 이러하니 Array가 클래스이고, arr가 Array클래스로 생성된 인스턴스라고 생각하는 것도 무리는 아니다.
그러나 '인스턴스 생성'은 '클래스 작동 계획을 실제 객체로 복사하는 것'이다.
이전 포스팅에서도 살펴봤듯이 자바스크립트는 복사에 친화적이지 않다.
(억지로 구현할 수는 있지만 이 역시 참조하는 메모리 주소값의 복사에 불과하다)
그리고 결정적으로 [[Prototype]]은 명백히 다른 객체를 '참조'하는 작동 원리를 가지고 있다.
결국 new 함수()는 새로 생성된 객체를 함수의 prototype과 '연결'시켰을 뿐이다.
(몇 번을 강조해도 지나치지 않다. 클래스와 인스턴스는 자바스크립트에 없다)
Array.prototype의 [[Prototype]] property가 Object.prototype과 연결되어 있음을 알 수 있다.
클래스 지향에 익숙한 사람은 이를 'Array클래스가 Object클래스를 상속받았다'라고 표현할 것이다.
하지만, 상속 역시 '복사'가 수반되는 작업이다.
자바스크립트는 두 객체를 연결했을 뿐이고, 이러한 연결 관계를 보다 정확하게 표현할 수 있는 용어는 '위임'이다.
function woobuntu() {
console.log("닉값하고 싶다");
}
var a = new woobuntu();
일반적으로 클래스 지향 언어에서는 new 생성자()의 형태로 인스턴스를 생성하기에, 위의 형태를 보면 마치 woobuntu가 생성자인 것처럼 느껴진다.
하지만, woobuntu함수의 내용을 살펴보면, 무언가를 '생성'하는 작업은 어디에도 정의되어 있지 않다.
prototype에 constructor라는 property가 정의되어 있기는 하지만, 이는 woobuntu함수 자신을 참조할 뿐이다.
결국, 새로운 객체를 생성하는 것은 new 키워드이다.
new키워드가 함수의 호출과 결합되면, 함수의 호출에 더해 함수의 prototype에 연결된 새로운 객체를 생성하는 것이다.
('닉값하고 싶다'가 출력됨과 동시에 a라는 새로운 객체가 생긴 것처럼)
ES6의 화살표 함수로 선언하면 prototype이 없기 때문에 new 키워드와 결합되면 에러가 발생한다.
클래스를 흉내내기 위해 클래스의 역할을 할 함수를 대문자로 선언하는 관습이 굳어지면서 마치 '클래스 지향'개념의 '생성자'라는 실체가 자바스크립트에도 있는 것처럼 보이지만, 실제로는 그렇지 않다.
(위의 woobuntu함수는 소문자로 선언했음에도 new 키워드와의 결합으로 새로운 객체를 생성하지 않았는가)
자바스크립트에 '생성자'는 없다.
다만, new 함수()의 형태로 호출하면, 함수의 prototype과 [[Prototype]]으로 연결된 새로운 객체가 생성될 뿐이다.
분명, new woobuntu()의 형태로 a라는 객체를 생성했는데, a.constructor는 Object.prototype을 가리킨다.
이는 woobuntu함수의 prototype을 {}로 재정의했기 때문이다.
앞서도 말했듯이, new 함수()의 형태로 호출하면 함수의 prototype과 [[Prototype]]으로 연결된 새로운 객체가 생성된다.
여기서 woobuntu의 prototype이 {} 이렇게 빈 객체이기 때문에 a의 [[Prototype]]은 빈 객체를 가리킨다.
a.constructor와 같이 접근하면, [[Get]]이 own property에서 constructor를 찾는다.
a의 own property에는 constructor가 없기에 [[Prototype]]을 타고 올라가지만, {}형태의 빈 객체이기에 이곳에도 constructor는 없다.
{}의 [[Prototype]]을 다시 타고 올라가면 Object.prototype이 연결되어 있고, constructor property를 찾을 수 있다.
그렇다면 이 Object.prototype.constructor, 즉 Object가 '생성자'로서 a 생성에 관여한 바가 있을까?
그런 거 없다.
a를 생성한 함수는 woobuntu이고 woobuntu의 prototype은 빈 객체이다.
따라서 빈 객체와 [[Prototype]]으로 연결된 a가 생성된 것이다.
a.constructor로 발견되는 property는 그저 [[Prototype]] chain상에 존재할 뿐이다.
심지어 constructor는 기본적으로 writable:true인 property이기에 "뭐 이래"같은 기본형 데이터로 덮어씌우는 것도 가능하다.
constructor라는 용어에 현혹되지 말자.
자바스크립트에서주목해야 할 것은 용어나 형태가 아니라 참조 구조이다.
function woobuntu() {}
var a = new woobuntu();
a instanceof woobuntu; // true
인스턴스 instanceof 클래스;
위와 같이 '객체 instanceof 함수'를 실행하면, a의 [[Prototype]] chain상에 함수의 prototype이 존재하는지를 확인한다.
이름에서부터 알 수 있듯이 '클래스 지향'의 개념을 그대로 끌고 온 연산자이다.
a와 woobuntu의 연결 관계를 파악하는 것처럼 보이지만 실제로는 a와 woobuntu.prototype의 연결 관계에 대한 값을 반환한다.
보다 더 직접적으로 비교할 수 있는 방법을 알아보자.
function woobuntu() {}
var a = new woobuntu();
woobuntu.prototype.isPrototypeOf(a);
얼핏 보면 더 복잡한 거 아닌가 싶지만, 직접 비교할 두 객체를 사용한다는 점에서 보다 합리적이다.
instanceof와 비교하기 위해 같은 예제를 사용했지만, Object.prototype.isProtoypeOf는 '두 객체가 [[Prototype]] chain상에서 연결되어 있는가'라는 역할을 충실히 이행한다.
'클래스와 인스턴스의 관계'가 아닌 '두 객체'의 관계를 비교한다는 점에서 범용성도 훨씬 높다.
(이후 '위임'형 예제에서 다시 보겠다)
개발자 도구에서 위와 같이 표시되기 때문에 마치 a에 __proto__ property가 있는 것 같다.
(a에 __proto__라는 property는 없다)
이는 내부 property인 [[Prototype]]을 개발자 도구상에서 편하게 볼 수 있게끔 하기 위한 브라우저의 배려(혹은 실수)로 보인다.
__proto__의 실체는 아래와 같다.
위 그림에서 알 수 있다시피 __proto__는 Object.prototype에 정의되어 있는 getter이자 setter이다.
YDKJS에서 추측하는 __proto__의 코드는 아래와 같다.
Object.defineProperty(Object.prototype, "__proto__", {
get: function () {
return Object.getPrototypeOf(this);
},
set: function (o) {
Object.setPrototypeOf(this, o);
return o;
},
});
즉, a.__proto__처럼 접근하는 것은 a를 this로 하여 getter 혹은 setter를 호출하는 것이다.
(this바인딩에 대해서는 추후 다른 포스팅에서 다룰 예정이다)
__proto__는 ES5까지는 비표준이었으며, ES6스펙에서 부록에 다루기는 했으나 사용하지 말라는 의미에서 명시한 것에 가깝다.
따라서 Object.getPrototypeOf(object)메소드를 사용하는 것을 권장한다.
앞서 봤듯이, [[Prototype]]은 결국 다른 객체를 참조하는 일종의 포인터이다.
이 [[Prototype]]으로 연결된 객체들, 혹은 이러한 구조를 [[Prototype]] chain이라고 한다.
var woobuntu = {
say: function () {
console.log("닉값하고 싶다");
},
};
var linux = Object.create(woobuntu);
linux.say();
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function () {
return "I am " + this.me;
};
function Bar(who) {
Foo.call(this, who);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function () {
alert("Hello, " + this.identify() + ".");
};
var b1 = new Bar("b1");
var b2 = new Bar("b2");
b1.speak();
b2.speak();
Foo = {
init: function (who) {
this.me = who;
},
identify: function () {
return "I am " + this.me;
},
};
Bar = Object.create(Foo);
Bar.speak = function () {
alert("Hello, " + this.identify() + ".");
};
var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");
b1.speak();
b2.speak();
단순히 코드가 짧아진 정도가 아니다.
.prototype이니 .constructor니 하는 복잡한 것들을 고려하지 않고, 오직 '연결'에만 집중하여 훨씬 직관적으로 코드를 짤 수 있다.
생성과 초기화의 단계를 구분하여 훨씬 유연하다.
또한, 이렇게 객체를 연결했을 때 Object.isPrototypeOf메소드를 활용한 관계 파악은 더욱 용이해진다.
Bar.isPrototypeOf(b1);
(아니 이 정도면 위임이 압도적으로 자바스크립트에 적합한 디자인 패턴 아닌가)
자바스크립트에서 객체는 [[Prototype]]이라는 내부 property를 갖는다.
이 [[Prototype]]은 해당 객체와 다른 객체를 연결한다.
클래스 지향을 흉내낸다고 .prototype이니 .constructor니 하는 것들을 구현해놨지만, 이런 것들을 다 걷어내면 결국 자바스크립트의 주된 작동방식은 '객체와 객체의 연결'이다.
다음 포스팅에서는 Function object에 대해 알아본다.