読者です 読者をやめる 読者になる 読者になる

JavaScriptプロトタイプマップ

最近JavaScriptを触ることが多いのですが、JavaScriptのプロトタイプについて調べてもすぐに忘れてしまうので、ちょっとまとめてみました。

プロトタイプベース

プロトタイプベースってそもそも何だろうと。僕の理解ではこんな感じです。

  1. オブジェクトの振る舞いはそのオブジェクト自身とそのオブジェクトが保持するプロトタイプオブジェクトによって決定される
  2. またプロトタイプオブジェクトもオブジェクトなので、その振る舞いも1.に従う
  3. 1.と2.よりオブジェクトの振る舞いはプロトタイプを連鎖的にたどることとなる(プロトタイプチェーン)
  4. オブジェクトは静的な構造と関連せず、自由にプロパティを上書き/追加/削除することができる
    • 同じコンストラクタから生成されたとしても、プロパティが同じとは限らない


以下のリンクが参考になるかと。

プロトタイプマップ

JavaScriptの組み込みオブジェクトとユーザが生成したオブジェクトのプロトタイプのマップを書いてみました。

  • 灰色のボックスはオブジェクト、水色のボックスはプロパティ、矢印は参照先を示します。
  • [Type: Name]
    • Typeコンストラクタによって生成されたNameオブジェクトであるということを示します
    • 例えば[Array: array]はArrayコンストラクタによって生成されたarrayという変数を表します。
var array = new Array();
//もしくは
var array = [];
  • __proto__プロパティ
    • オブジェクトが内部的に持っているプロトタイプオブジェクトを示します
    • 一部のJavaScript実装には__proto__プロパティが存在しますが、ECMAScript5では非標準です
    • ECMAScript5ではObject.getPrototypeOf(object)でobjectのプロトタイプオブジェクトを取得します
var array = new Array();
console.log(Object.getPrototypeOf(array) === Array.prototype); //true
  • constructorプロパティ
    • オブジェクトの生成元コンストラクタ(関数)を示します
    • constructorプロパティはオブジェクト自身のプロパティではなく、自身のプロトタイプオブジェクトのconstructorプロパティを参照することがほとんどです
var array = new Array();
console.log(array.constructor === Array); //true
console.log(array.hasOwnProperty("constructor")); //false
console.log(Object.getPrototypeOf(array).hasOwnProperty("constructor")); //true

オブジェクトの生成

オブジェクトの生成はこんな感じ。

var array = new Array();
  1. new演算子によって空オブジェクトが生成される。
  2. コンストラクタ関数(Array)のprototypeオブジェクトが空オブジェクトのプロトタイプオブジェクトにセットされる(__proto__の設定)
  3. コンストラクタ関数によって空オブジェクトを初期化する(プロパティの追加など)
  4. 初期化された空オブジェクトが返される

プロトタイプチェーン

オブジェクトのプロパティにアクセスしたときの動作はこんな感じ。

array.hoge();
  1. arrayオブジェクト自身にhogeプロパティが存在するか確認する
  2. 存在しない場合はarrayオブジェクトのプロトタイプオブジェクトにhogeプロパティが存在するか確認する
  3. 存在しない場合はプロトタイプオブジェクトのプロトタイプオブジェクトのにhogeプロパティが存在するか確認する
  4. 3.をhogeプロパティが存在するか、プロトタイプオブジェクトがnullになるまで確認する(プロトタイプチェーン)
    • 擬似的に書くとarray.__proto__.__proto__...と確認していく
  5. 最終的にnullに行き着いた場合はプロパティが存在しない、存在した場合はその値を返す。

※参照ではなく代入の場合プロトタイプチェーンを使わず、オブジェクト自身に新しくプロパティがセットされる。

プロトタイプチェーンの変更

オブジェクトのプロトタイプチェーンを動的に変更してみるとこんな感じ。

//Hogeコンストラクタ関数
var Hoge = function(msg){ this.msg = msg; };
Hoge.prototype.say = function(){ console.log(this.msg);};

//Fooコンストラクタ関数
var Foo = function(msg){ this.msg = msg; };
Foo.prototype.say = function(){ console.log(this.msg + "!!!!!!!!!!!!"); };

//Hogeコンストラクタ関数によりhogeオブジェクトを生成
var hoge = new Hoge("hoge hoge");
hoge.say(); //hoge hoge
console.log(hoge instanceof Hoge); //true

//hogeオブジェクトのコンストラクタ関数を変更してみる
hoge.constructor = Foo;
hoge.say(); //hoge hoge
console.log(hoge instanceof Hoge); //true

//hogeオブジェクトのプロトタイプオブジェクトを変更してみる
hoge.__proto__ = Foo.prototype;
hoge.say(); //hoge hoge!!!!!!!!!!!!
console.log(hoge instanceof Hoge); //false
console.log(hoge instanceof Foo); //true

注意すべきなのは以下の点。

  • オブジェクトのconstructorプロパティを変更してもプロトタイプチェーンに全く影響を与えない
  • __proto__を変更することでプロトタイプチェーンを変更できる
    • だけど、__proto__はECMAScript5では非標準。アクセスするにはObject.getPrototypeOf(hoge)を使う必要がある
      • だけどだけど、参照はできてもプロトタイプオブジェクト自体を変更することができない(getterはあるけど、setterはない)
        • あれ?オブジェクト生成したあとにプロトタイプチェーンを変更するのはできないの??(教えてエロイ人!)

まとめ

という訳でJavaScriptにおけるプロトタイプについて調べてみたわけですが、非常に面白いなと思いました。
クラスベースも内部的にはこいう継承チェーン構造を持っているんだろうけど、それをユーザレベルで見られるとwktkします。
あと気になったのはところどころ循環参照っぽい感じになってますね。例えば[Function: anonymous]のコンストラクタは[Function: Function]を指していて、[Function: Function]のプロトタイプオブジェクトは[Funtion: anonymous]を指してる。でもプロトタイプチェーンにはコンストラクタは関係ないから、実際は循環参照にはならないと。(いや、そもそも[Function: Function]自体がw)


次はこのプロトタイプを使って継承や擬似クラスベースみたいなことをやってみたいと思います。
(まあクラスベースはプロトタイプベースの特殊系と言えそうなので、擬似じゃなくて本物も作れるのかも)


間違い等々ありましたら教えて下さい。
ではでは。