[[Prototype]]

WooBuntu·2020년 8월 10일
0




  • 자바스크립트는 객체 지향 언어이며, 객체가 행위와 속성을 가진다는 개념을 object가 property를 가진다는 것으로 구현한다.
    (이것이 지난 포스팅까지 다룬 개념이다)

  • 이번 포스팅에서는 서로 다른 두 객체가 [[Prototype]]을 통해 어떻게 연결되는지에 대해 알아본다.

  • 객체에는 [[Prototype]]이라는 내부 property가 있다.

  • 이 [[Prototype]]은 수임객체를 참조한다.

    • 개발자가 따로 처리하지 않는다면, 객체 생성 시점에 [[Prototype]]에 참조할 객체의 메모리 주소값이 할당되기 마련이다

    • 거꾸로 말하면, 개발자가 따로 처리를 하는 경우 [[Prototype]]이 다른 객체를 참조하지 않게끔 만들 수도 있다는 것이다.

  • 이때 참조되는 객체를 수임객체라고 하고, 참조하는 객체를 위임객체라고 한다.

    • '위임'과 '수임'이라는 용어를 쓰는 이유는 메소드 호출의 사례를 '클래스 지향'과 비교해보면 유추할 수 있다.

    • '클래스 지향'에서는 인스턴스의 생성도, 클래스의 상속도 모두 '복사'로 이루어진다.

    • 따라서 인스턴스에서 특정 메소드를 호출하는 것은 복사된 메소드, 즉 자신의 메소드를 호출하는 것이다.

    • 반면 자바스크립트에서는 위임객체의 own property에 찾는 메소드가 없으면 [[Prototype]]을 타고 올라가 수임객체의 메소드를 호출한다.

    • 즉, 위임객체가 메소드의 작동을 수임객체에게 '위임'하는 것이다.
      (호출의 주체는 위임객체이다. 이는 this바인딩에서 더욱 자세하게 다룰 것이다)

  • 일반적으로 함수의 prototype이 참조되는 것이지, prototype이 곧 수임객체인 것은 아니다.

  • 지난 포스팅에서 object의 property에 접근할 때, object의 [[Get]]을 호출한다고 했다.

  • 찾는 property가 own property에 없다면, [[Get]]은 [[Prototype]]을 타고 올라가 property를 찾는다.

1. Object.prototype

  • [[Prototype]]에 별도의 처리를 하지 않았다면, [[Prototype]] chain의 종착지는 Object.prototype이다.

2. property setting은 own property에서만!

  • [[Prototype]] chain을 통해 수임객체의 property에 접근할 수 있으니 이를 이용해서 수임객체의 property를 바꾸면 되겠다고 생각할 수도 있다.

  • 결론부터 말하자면 이는 지양해야 할 행위이며, 얼마나 복잡한 일인지 아래의 예제를 통해 살펴보자.

  • 예제가 다소 장황하기 때문에 위임객체에서 수임객체의 property를 바꾸려고 하지 말자는 것만 기억하고 1-3파트로 넘어가도 무방하다.

  1. 첫번째로 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이 할당되었다.
      (누가 설계했냐)

  2. 두번째로 bun property에 대해 살펴보자.

    • Object.prototype에 bun property를 writable:false로 설정했다.

    • obj에는 bun이 없으니 obj["bun"]은 Object.prototype.bun을 가리킬 것이고,

    • writable값이 false이니 Object.prototype.bun의 값은 변경되지 않는다.

  3. 세번째로 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인 경우에만 가능하다)

3. 클래스

  • 첫 포스팅에서 이번 시리즈는 온전히 자바스크립트의 관점에서 '객체 지향' 개념을 서술할 것이라고 했다.

  • YDKJS를 읽어본 결과, 자바스크립트의 본질에 가까운 디자인 패턴은 '클래스 지향'이 아니라 '위임'이다.

  • 그러나 개발자들 사이에서 자바스크립트로 '클래스 지향'을 흉내내는 것이 오랜 관습처럼 굳어져서인지 대부분의 책에서는 [[Prototype]] chain마저 '클래스 지향'의 관점에서 서술하고 있다.

  • 오히려 '위임'에 대한 자료보다 이런 '클래스 지향' 관점의 자료가 압도적으로 많기에 일단 [[Prototype]] chain은 '클래스 지향'의 관점을 곁들여 설명하고자 한다.

  • 하지만 자바스크립트의 주된 작동 방식이 '복사'가 아닌 '참조'라는 것을 항상 염두에 둬야 하며, 최대한 이를 반영해서 서술할 것이다.

  • 위임 디자인 패턴의 개념에 대해서는 마지막에 아주 간략하게 언급만 하고 넘어가고자 한다.
    (이번 시리즈의 목적은 어디까지나 자바스크립트의 이해이기 때문이며,
    디자인 패턴에 대해서는 이후에 '함수형 프로그래밍'시리즈로 작성하고자 한다)

클래스 함수

  • 자바스크립트에서 '클래스' 혹은 '생성자'와 같은 용어가 남발되는 이유는 ES6의 화살표 함수를 제외한 모든 함수가 prototype이라는 property를 가진다는 점에서 기인한다.

  • Array와 같은 built-in object는 물론이고,
function Woobuntu() {};

  • 이와 같이 개발자가 새로 정의한 함수에도 prototype이라는 property가 자동으로 생성된다.

  • 이제 Array 함수를 new 키워드와 함께 사용해보자.

  • new 키워드와 함수를 함께 사용해 생성해낸 객체인 arr는 Array.prototype에 정의된 concat메소드를 쓸 수 있다.

  • 보이는 것이 이러하니 Array가 클래스이고, arr가 Array클래스로 생성된 인스턴스라고 생각하는 것도 무리는 아니다.

  • 그러나 '인스턴스 생성'은 '클래스 작동 계획을 실제 객체로 복사하는 것'이다.

  • 이전 포스팅에서도 살펴봤듯이 자바스크립트는 복사에 친화적이지 않다.
    (억지로 구현할 수는 있지만 이 역시 참조하는 메모리 주소값의 복사에 불과하다)

  • 그리고 결정적으로 [[Prototype]]은 명백히 다른 객체를 '참조'하는 작동 원리를 가지고 있다.

  • 결국 new 함수()는 새로 생성된 객체를 함수의 prototype과 '연결'시켰을 뿐이다.
    (몇 번을 강조해도 지나치지 않다. 클래스와 인스턴스는 자바스크립트에 없다)

    • 심지어 new 함수()는 두 객체를 '간접적으로' 연결하는 아주 번거로운 방식이다.
    • Object.create함수를 사용하여 두 객체를 직접 연결하는 것이 훨씬 좋은 방식이다.
      (밑에서 후술한다)

  • 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라는 용어에 현혹되지 말자.
    자바스크립트에서주목해야 할 것은 용어나 형태가 아니라 참조 구조이다.

수임객체와 위임객체의 관계(Introspection/Reflection)

instanceof 연산자

function woobuntu() {}

var a = new woobuntu();

a instanceof woobuntu; // true

인스턴스 instanceof 클래스;
  • 위와 같이 '객체 instanceof 함수'를 실행하면, a의 [[Prototype]] chain상에 함수의 prototype이 존재하는지를 확인한다.

  • 이름에서부터 알 수 있듯이 '클래스 지향'의 개념을 그대로 끌고 온 연산자이다.

  • a와 woobuntu의 연결 관계를 파악하는 것처럼 보이지만 실제로는 a와 woobuntu.prototype의 연결 관계에 대한 값을 반환한다.

  • 보다 더 직접적으로 비교할 수 있는 방법을 알아보자.

Object.prototype.isPrototypeOf

function woobuntu() {}

var a = new woobuntu();

woobuntu.prototype.isPrototypeOf(a);
  • 얼핏 보면 더 복잡한 거 아닌가 싶지만, 직접 비교할 두 객체를 사용한다는 점에서 보다 합리적이다.

  • instanceof와 비교하기 위해 같은 예제를 사용했지만, Object.prototype.isProtoypeOf는 '두 객체가 [[Prototype]] chain상에서 연결되어 있는가'라는 역할을 충실히 이행한다.

  • '클래스와 인스턴스의 관계'가 아닌 '두 객체'의 관계를 비교한다는 점에서 범용성도 훨씬 높다.
    (이후 '위임'형 예제에서 다시 보겠다)

__proto__

  • 개발자 도구에서 위와 같이 표시되기 때문에 마치 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)메소드를 사용하는 것을 권장한다.

4. 객체 연결

  • 앞서 봤듯이, [[Prototype]]은 결국 다른 객체를 참조하는 일종의 포인터이다.

  • 이 [[Prototype]]으로 연결된 객체들, 혹은 이러한 구조를 [[Prototype]] chain이라고 한다.

연결

var woobuntu = {
  say: function () {
    console.log("닉값하고 싶다");
  },
};

var linux = Object.create(woobuntu);

linux.say();
  • Object.create은 빈 객체를 생성한 뒤, [[Prototype]]을 통해 인자로 전달된 객체와 연결한다.

  • 그림을 보면 알 수 있듯이, .prototype이니 .constructor니 하는 것들 없이 심플하게 두 객체를 연결한 것이다.

5. 클래스 vs 위임

  • YDKJS에서 클래스와 위임의 코드를 비교 대조한 것이 있어 소개하고자 한다.

클래스 지향형 코드

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클래스를 상속하는 Bar클래스를 만들어 사용하는 코드이다.

위임형 코드

  • 앞서 '객체 연결'에서 다룬 방법을 활용하여 위임형 디자인 패턴으로 짠 코드이다.
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);
  • 클래스 디자인 패턴으로 짰다면 Bar.prototype.isPrototypeOf(b1)와 같이 장황해졌을 것이다.

(아니 이 정도면 위임이 압도적으로 자바스크립트에 적합한 디자인 패턴 아닌가)

이번 장의 결론

  • 자바스크립트에서 객체는 [[Prototype]]이라는 내부 property를 갖는다.

  • 이 [[Prototype]]은 해당 객체와 다른 객체를 연결한다.

  • 클래스 지향을 흉내낸다고 .prototype이니 .constructor니 하는 것들을 구현해놨지만, 이런 것들을 다 걷어내면 결국 자바스크립트의 주된 작동방식은 '객체와 객체의 연결'이다.

  • 다음 포스팅에서는 Function object에 대해 알아본다.

0개의 댓글