JavaScript How Prototypal Inheritance really works

이글은 Christopher Chedeau님의 'JavaScript How Prototypal Inheritance really works'을 번역한 것이다. 이 글은 JavaScript의 __proto__을 잘 정리설명한다.

prototypal inheritance에 대한 글은 다양한 방법이 도처에 널려있다. JavaScript는 이미 new를 이용한 prototypal inheritance를 지원하기 때문에 prototypal inheritance를 정확히 이해하기 힘들다. 이 글은 JavaScript의 prototypal inheritance를 명확히 설명하고자 한다.

Prototypal Inheritance Definition

prototypal inheritance에 대한 글을 읽을 때 다음과 같은 정의를 자주 볼 수 있다:

객체의 property에 접근할 때 JavaScript는 해당 property를 찾을 때까지 prototype chain을 따라 찾는다. - JavaScript Garden

많은 JavaScript 구현체들은 [__proto__][] 프로퍼티를 이용해 prototype chain을 만든다. 이 글에서는 먼저 __proto__ 와 prototype의 차이를 살펴 본다.

Note: __proto__는 표준이 아니기 때문에 직접 사용하지 말아야 한다. 이 글에서 __proto__를 사용하는 것은 JavaScript가 inheritance를 어떻게 처리하는지 설명하기 위해 사용하는 것이다.

다음 코드는 JavaScript 엔진이 property를 어떻게 찾는지 보여주는 코드다(설명용):

function getProperty(obj, prop) {
  if (obj.hasOwnProperty(prop))
    return obj[prop]

  else if (obj.__proto__ !== null)
    return getProperty(obj__proto__, prop)

  else
    return undefined
}

2차원 Point 객체의 예를 보자. 이 Point 객체는 x,y 좌표와 print method를 가진다. 위에서 설명한 prototypal inheritance의 정의에 따라서 x, y, print property를 만든다. Point를 만들기 위해서 객체를 새로 만들고 __proto__에 Point를 할당한다:

var Point = {
  x: 0,
  y: 0,
  print: function () { console.log(this.x, this.y); }
};

var p = {x: 10, y: 20, __proto__: Point};
p.print(); // 10 20

JavaScript Weird Prototypal Inheritance

바로 위와 같은 코드는 보여주지도 않고 prototypal inheritance를 가르치는 것이 문제다. 사람들은 위와 같은 코드 대신 다음과 같은 코드로 설명한다:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype = {
  print: function () { console.log(this.x, this.y); }
};

var p = new Point(10, 20);
p.print(); // 10 20

두 코두는 완전히 다르다. Point는 함수인데 prototype property와 new를 사용한다. 이게 뭥미?

How new works

Brendan Eich는 JavaScript를 Java나 C++같은 Object Oriented programming language로 만들고 싶어 했다. new 연산자는 class의 instance를 만드는 것이다. 그래서 Brendan Eich은 다음과 같이 new를 정의했다:

  • C++에 있는 instance의 attribute를 초기화하는 constructor처럼 new할 때 호출할 함수가 하나 필요하다.
  • 이 함수는 객체 어딘가에 있어야 한다. JavaScript는 prototypal language이기 때문에 함수의 prototype property에 두도록 하자.

new는 arguments가 있는 함수 F가 필요하다: new F(arguments…). new 세단게로 나뉘어 진행된다:

  • class의 instance를 만든다. 이때 만들어진 객체의 __proto__에 F.prototype이 할당된다.
  • 인자와 함께 함수 F를 호출하여 instance를 초기화한다.
  • instance를 반환한다.

new가 무엇을 하는지 설명했다. JavaScript코드로 이 내용을 구현해보자:

function New (f) {
  var n = { '__proto__': f.prototype }; /*1*/
  return function () {
    f.apply(n, arguments);              /*2*/
    return n;                           /*3*/
  };
}

그리고 이게 잘되나 한번 테스트 해보자:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype = {
  print: function () { console.log(this.x, this.y); }
};

var p1 = new Point(10, 20);
p1.print(); // 10 20
console.log(p1 instanceof Point); // true

var p2 = New (Point)(10, 20);
p2.print(); // 10 20
console.log(p2 instanceof Point); // true

Real Prototypal Inheritance in JavaScript

new가 어떤 것인지 JavaScript specifications에 설명돼 있지만 Douglas Crockford는 real Prototypal Inheritance를 하는 new를 만들었다. 그는 Object.create를 다음과 같이 구현했다:

Object.create = function (parent) {
  function F() {}
  F.prototype = parent;
  return new F();
};

이 것은 믿기 어려울 정도로 단순하다. 쉽게 객체를 만들고 prototype에 원하는 parent를 할당할 수 있게 돼있다. 만약 __proto__를 직접 사용한다고 가정하면 더 단순하다:

Object.create = function (parent) {
  return { '__proto__': parent };
};

다음 real prototypal inheritance를 사용한 예이다:

var Point = {
  x: 0,
  y: 0,
  print: function () { console.log(this.x, this.y); }
};

var p = Object.create(Point);
p.x = 10;
p.y = 20;
p.print(); // 10 20

Conclusion

지금까지 prototypal inheritance가 무엇인지, JavaScript가 어떻게 구현했는지 살펴봤다.

그러나 real prototypal inheritance (Object.create and __proto__)에는 단점이 있다:

  • 비표준: __proto__는 표준도 아니고 심지어 deprecated됐다. 그리고 native Object.create와 Douglas Crockford의 구현은 다르다.
  • 최적화 되지 않음: Object.create (native or custom)는 아직 최적화되지 않았다. 이 코드는 약 10배 느린 경우도 있다.

더 읽을 만한 글들:

역주, 원작자가 주장하는 바가 무었인지 명확하지는 않다. 실제로 조사를 해보면 너무 헷갈리다. 하지만 __proto__와 prototype에 대해서는 아주 잘 정리했다.