しばしば JavaScript におけるクラス継承機構として紹介される、プロトタイプチェーンについて。
ECMAScript では、式 o.p
を用いて o の値に設定されたオブジェクト O のプロパティ p にアクセスしようとするとき、 O が名前 p のプロパティを持たなければ、 O のプロトタイプであるオブジェクト O1 から名前 p のプロパティを検索する。 O1 も名前 p のプロパティを持たなければ、さらに O1 のプロトタイプであるオブジェクト O2 から名前 p のプロパティを検索する。以下同様にオブジェクトのプロトタイプが null になるまでこれを繰り返し、それでも見つからなければ undefined を返す。これを プロトタイプチェーン という。
オブジェクトのプロトタイプは生成時に決定され、コンストラクタ関数の prototype プロパティの値に設定される。オブジェクトのプロトタイプを設定する方法はこれ以外にはない。(*1)
では、このことを実際に試してみよう。関数 CF を定義して、それをコンストラクタとして呼出し、生成したオブジェクトを cf1 に設定する。
// コンストラクタ関数 CF function CF () { this.q1 = 'q1 by CF'; this.q2 = 'q2 by CF'; } // CF からオブジェクト cf1 を生成 var cf1 = new CF(); // cf1 のプロパティを出力 document.writeln( 'cf1.q1 = ' + cf1.q1 ); document.writeln( 'cf1.q2 = ' + cf1.q2 );
オブジェクトが生成され、 CF によって q1, q2 というプロパティが追加され、 cf1 の値に設定された。
ここで、新しいオブジェクトを生成し CFP に設定し、 CFP1 というプロパティを作成してみる。
// 新規オブジェクト生成 var CFP = new Object(); CFP.CFP1 = 'CFP.CFP1'; CFP.q1 = 'CFP.q1'; // CFP, cf1 のプロパティ CFP1 を出力 document.writeln( 'CFP.CFP1 = ' + CFP.CFP1 ); document.writeln( 'cf1.CFP1 = ' + cf1.CFP1 );
当然のことだが、 cf1 から CFP のプロパティ CFP1 を参照することは出来ない。 CFP は cf1 のプロトタイプではないから。
そこで、 CF.prototype を CFP の値に設定し、改めてオブジェクトを生成してみよう。
// CF の prototype プロパティを設定 CF.prototype = CFP; // 改めて CF からオブジェクトを生成 var cf2 = new CF(); // cf2 のプロパティを出力 document.writeln( 'cf2.q1 = ' + cf2.q1 ); document.writeln( 'cf2.q2 = ' + cf2.q2 ); document.writeln( 'cf2.CFP1= ' + cf2.CFP1 );
CF.prototype プロパティが CFP の値になったので、CF から生成されたオブジェクトのプロトタイプは CFP の値に設定される。 cf2 は、 CF によって作成されたプロパティ q1, q2 を参照できるだけでなく、プロトタイプのプロパティ CFP1 にも自分のプロパティのであるかのように参照できる。これが、プロトタイプチェーンだ。
新たに関数 CF2 を定義し、 prototype に今生成した cf2 を設定してみる。この CF2 からオブジェクト cf3 を生成してみよう。
// コンストラクタ関数 CF2 function CF2 () { this.q1 = 'q1 by CF2'; } // CF2 の protorype プロパティを設定 CF2.prototype = cf2; // CF2 から cf3 を生成 var cf3 = new CF2(); // cf3 のプロパティを出力 document.writeln( 'cf3.q1 = ' + cf3.q1 ); document.writeln( 'cf3.q2 = ' + cf3.q2 ); document.writeln( 'cf3.CFP1 = ' + cf3.CFP1 );
cf3.q1 は cf3 自身が q1 プロパティを持っているので、これを参照する。 cf3.q2 は cf3 自身がこのプロパティを持たないので、 cf2 のプロパティ q2 を参照する。 cf2 は cf3 のプロトタイプだ。 cf3.CFP1 は CFP のプロパティ CFP1 を参照する。 CFP は cf2 のプロトタイプだ。
cf3, cf2, CFP から順に q1 プロパティを delete していくとこのようになる。
document.writeln( 'cf3.q1 = ' + cf3.q1 ); // 直接のプロパティを削除 delete(cf3.q1); document.writeln( 'cf3.q1 = ' + cf3.q1 ); // プロトタイプのプロパティを削除 delete(cf2.q1); document.writeln( 'cf3.q1 = ' + cf3.q1 ); // プロトタイプのプロトタイプのプロパティを削除 delete(CFP.q1); document.writeln( 'cf3.q1 = ' + cf3.q1 );
ところで、コンストラクタ CF の prototype プロパティは現在オブジェクト CFP になっているわけだが、最初に CF から作成した cf1 は CFP.CFP1 プロパティを参照できるだろうか。
// cf1 のプロパティ CFP1 を出力 document.writeln( 'cf1.CFP1 = ' + cf1.CFP1 );
結果は undefined のはずである。cf1 のコンストラクタ CF.prototype の値がプロパティ CFP1 を持っていても、 cf1 はこれを参照できない。どういうことだろうか?
結論を言うと、現在の CF.prototype の値と cf1 のプロトタイプは別のオブジェクトだということだ。オブジェクトのプロトタイプは生成時にコンストラクタの prototype プロパティの値であったオブジェクトだが、コンストラクタの prototype の値を常に監視しつづけるわけではない。つまり、後から CF.prototype の値を別なオブジェクトに設定しても、それは CF.prototype の値の変更であって、 cf1 のプロトタイプオブジェクトの変更を意味しない。だから、 CF.prototype として新たに設定された CFP にどんな名前のプロパティがあっても cf1 のプロトタイプチェーンに出現することはなく、 cf1 から CFP のプロパティを参照できたりはしない。 cf1 は CF から作成されたオブジェクトであっても、 CF.prototype のプロパティを共有できない。
逆に、こんなコードが成立する。
function CF(){} var P1 = new Object(); P1.x = 'P1.x'; var P2 = new Object(); P2.x = 'P2.x'; CF.prototype = P1; var cf1 = new CF(); CF.prototype = P2; var cf2 = new CF(); CF.prototype = P1; var cf3 = new CF(); document.writeln( 'cf1.x = ' + cf1.x ); document.writeln( 'cf2.x = ' + cf2.x ); document.writeln( 'cf3.x = ' + cf3.x );
言葉を変えれば、一つのコンストラクタで生成するオブジェクトに様々なオブジェクトを継承させることが可能ということになる。他の言語では考えられないようなプログラミングもできるだろう。
つまり、よく言われる 「prototype のプロパティは、そのコンストラクタに生成された全オブジェクトで共有される」 は、説明として不十分だ。「prototype のプロパティは、 同じオブジェクトを prototype として そのコンストラクタに生成された全オブジェクトで共有される」というのがより正確だろう。
以上、「prototype はクラスの共有プロパティ」 という前提は 100% は成立しない、と言う話でした。
*1 JavaScript1.3 以降(JScriptを除く) では、全オブジェクトが __proto__ プロパティを持ち、自分自信のプロトタイプオブジェクトの取得/再設定が可能になっている。 [2001-12-22]
Issued: / Revised: / All rights reserved. © 2002-2017 TAKI