30 Sep 2010

prototype と __proto__

背景

JavaScript の オブジェクト指向はプロトタイプベースであるというのは有名な話です. prototype プロパティに独自定義のメソッドやプロパティを突っ込んでおくと new して使える, くらいのことをなんとなく理解していて使ってはいたんですが, よくよく考えるときちんとは分かっていませんでした. 特に prototype と __proto__ まわりで混乱したので, 自分の理解のためにまとめてみました.

ポイント

要点は以下の3つです.

  • prototype と __proto__ は別物
  • いわゆる "プロトタイプチェーン" は __proto__ プロパティで実現されている
  • オブジェクトを new するとき, コンストラクタ関数の prototype プロパティが指しているオブジェクトが, 生成されるオブジェクトの __proto__ に代入される

JavaScript のオブジェクトのプロパティにアクセスしようとしたとき, まずは自分のオブジェクトにそのプロパティがあるかどうか, 次にプロトタイプにあるかどうか, つぎはプロトタイプのプロトタイプにあるかどうか, というふうに, 処理系は順にプロパティを探索します. このようなプロトタイプのつながりは, ちょうどプロトタイプが鎖のように連なっているので, プロトタイプチェーンとよばれます.

ここで, 何となくプロトタイプという概念がでてきましたが, これは実際には __proto__ というプロパティによって実現されています. __proto__ はすべてのオブジェクトが持つプロパティです. 処理系はそのオブジェクトに対象のプロパティが見つからなかった場合は, __proto__ が指すオブジェクトを次に探索します. 見つからない場合は __proto__.__proto__, __proto__.__proto__.__proto__ ... といった具合にさらに遡っていきます. この探索は __proto__ の値が null になるまで続きます. Object.prototype.__proto__ には null が入っているので, ここで探索は打ち切られます. Object オブジェクトははすべてのオブジェクトの元となっているので, ここが探索の終点になるわけです.

ちなみにこの __proto__ というプロパティへのアクセスは標準ではく, 実装依存になるので注意が必要です. 開発者がアクセスできないかもしれないというだけで, プロトタイプの動作の理解には問題ありません.

次に注目するのは, コンストラクタと prototype プロパティです. new 演算子に関数名を渡すと, その関数がコンストラクタとして呼ばれ, 新しいオブジェクトが返されます. このとき, コンストラクタ関数の prototype プロパティが指しているオブジェクトが, 新しく生成されるオブジェクトの __proto__ プロパティに代入されます. コンストラクタ関数が実行されると同時に, 以下の処理が行われると考えて問題ありません.

// var o = new Obj() とした場合
o.__proto__ = Obj.prototype;

よって new した直後は prototype === __proto__ になっています. 当然ですがそのあとオブジェクトの prototype を変更した場合はその限りではありません.

prototype を prototype へいれているわけではないということです. 個人的にはここにつまっていました.

いろいろやってみる

function Obj() {
  this.x = 1;
}

Obj.prototype.y = function() {
  return 'method y';
}

console.log(Obj.prototype === Obj.__proto__);  // false
console.log(Obj.prototype);  // y というプロパティをもつオブジェクト
console.log(Obj.__proto__);  // 空の関数
console.log(Obj.prototype.hasOwnProperty('y'));  // true

y というプロパティは Obj オブジェクトの prototype のプロパティなので Obj.prototype.hasOwnProperty('y') のときのみ true となります. Obj.__proto__ には空の関数が入っていたのですが, これはよく分かりませんでした. 調べてみると JavaScript のネイティブオブジェクトの __proto__ には空の関数が入っているようです.

function Obj() {
  this.x = 1;
}

Obj.prototype.y = function() {
  return 'method y';
}

var o = new Obj();

console.log(o.prototype);  // undefined
console.log(o.__proto__);  // y というプロパティをもつオブジェクト
console.log(o.__proto__ === Obj.prototype);  // true
console.log(o.hasOwnProperty('y'));  // false
console.log(o.__proto__.hasOwnProperty('y'));  // true
console.log(o.hasOwnProperty('x'));  // true

Obj を new し o に代入した直後には, 当然 o.prototype には何もはいっていないので undefined になります. Obj.prototype は o.__proto__ に入っています. y というプロパティは o.__proto__ に入っているので, o の own なプロパティではありません.

/**
* 以下のコードは Chrome の Developer Tool で実行してみてください
* Firebug で コンソールにオブジェクトを表示した場合, enumerable なプロパティしか見ることができないため (少なくともデフォルトでは)
* Chrome だと enumerable じゃないプロパティも見ることができました
*/

Object.prototype  // Object オブジェクトのメソッドが入っている. toString() とか
Object.__proto__  // 空の関数が入っている
Object.prototype.__proto__  // null. プロトタイプチェーンを辿る動作はここでストップする
Array.prototype  // Array オブジェクトのメソッドが入っている
Array.prototype.__proto__ === Object.prototype  // true

new して使えるように, JavaScript のネイティブオブジェクトの各メソッド・プロパティは prototype 内で定義されています. 上述したように __proto__ に入っている空の関数は詳細不明です.

考察

どうしてこのような実装になっているんでしょうか. すぐに思いつくのは, 2段階以上で継承する場合に対応するためです. もし prototype と __proto__ がわかれておらず同じものだった場合, 親オブジェクトの prototype のプロパティがすべて子オブジェクトの prototype に引き継がれていきます. 子から孫, 孫から曾孫へと継承していった場合, 親・子・孫の prototype のプロパティすべてが曾孫にも引き継がれてしまい, 効率的ではありません. よって "親から引き継いだ物を指すプロパティ (__proto__)" と "子へ引き継ぎたいものを含むプロパティ (prototype)" を分ける必要がある, ということなのかもしれません.

まとめ

  • prototype と __proto__ は別物
  • いわゆる "プロトタイプチェーン" は __proto__ プロパティで実現されている
  • オブジェクトを new するとき, コンストラクタ関数の prototype プロパティが指しているオブジェクトが, 生成されるオブジェクトの __proto__ に代入される

参考