JavaScript: prototypeをようやく理解した
基本
例えば、こうすると
var F = function(){};
F.prototype = { a: "F.prototype.a" };
var f = new F();
f.a
// -> "F.prototype.a"
f.b
// -> undefined
F.prototype.b = "F.prototype.b"; f.b // -> "F.prototype.b"
こうなる。
prototypeを変更すると、それをもつオブジェクトに(既にnewされたものも含め)すべて反映される。
親プロパティの継承を実現するには
さらにprototypeに「prototypeをもつオブジェクト」をセットすると
var F2 = function(){};
F2.prototype = new F();
/*
(間違い)
コンストラクタを呼び出す必要がなければ
F2.prototype = F.prototype;
でもよい
*/
var f2 = new F2();
[f2.a, f2.b]
// -> ["F.prototype.a", "F.prototype.b"]
f2.c
// -> undefined
F.prototype.c = "F.prototype.c"; f2.c // -> "F.prototype.c"
このようにF2.prototypeにF.prototypeの変更が反映され、その結果f2からも参照できるようになる。
プロトタイプチェイン。
というわけで、親プロパティの継承を実現するには「prototypeをもつオブジェクト」(あるいはprototypeそのもの)をprototypeにセットすれば良い。
親コンストラクタを呼ぶには
var F3 = function(){
F2.apply(this, arguments);
// F3のコンストラクト処理など
};
var F4 = function(){
F3.apply(this, arguments);
// -> F3のコンストラクタを呼び、そこからF2のコンストラクタも呼ばれる
};
こうやって、親コンストラクタを子のコンテキストで呼ぶ。
Function#applyは関数をコンテキストを指定して実行する。
引数はすべてargumentsがもっているので、(子コンストラクタが必要としないのであれば)仮引数は記述しなくても良い(けど、あとから見てもわかるように記述しておいたほうが良いと思う)。
テスト
var F = function(){};
F.prototype.a = "F.prototype.a";
var F2 = function(){};
F2.prototype = new F();
F2.prototype.b = "F2.prototype.b";
var F3 = function(){};
F3.prototype = new F2();
F3.prototype.c = "F3.prototype.c";
var f3 = new F3();
[f3.a, f3.b, f3.c, f3.d]
// -> ["F.prototype.a", "F2.prototype.b", "F3.prototype.c", undefined]
F.prototype.d = "F.prototype.d";
[f3.a, f3.b, f3.c, f3.d]
// -> ["F.prototype.a", "F2.prototype.b", "F3.prototype.c", "F.prototype.d"]
// (間違い)
var F = function(){};
F.prototype.a = "F.prototype.a";
var F2 = function(){};
F2.prototype = F.prototype;
F2.prototype.b = "F2.prototype.b";
var F3 = function(){};
F3.prototype = F2.prototype;
F3.prototype.c = "F3.prototype.c";
var f3 = new F3();
[f3.a, f3.b, f3.c, f3.d]
// -> ["F.prototype.a", "F2.prototype.b", "F3.prototype.c", undefined]
F.prototype.d = "F.prototype.d";
[f3.a, f3.b, f3.c, f3.d]
// -> ["F.prototype.a", "F2.prototype.b", "F3.prototype.c", "F.prototype.d"]
まとめ
prototypeを何となく「newしたときにコピーされるもの」と考えるのは大間違いなので悔い改めよ、というお話。
追記
揚げ足を取るようで嫌なのだが、弾さんの以下の例では継承は行われない。
MyBox.prototype = Box.prototype; // ここで継承しているのに「勝手に添削 - JavaScript 入門」を勝手に添削 - IT戦記
もうちょっと調べる。
関数一発でプロトタイプチェーンに繋げて、オブジェクトをクローンする。 - IT戦記
コンストラクタを操作できないので少し話は違うけど、すごい抽象化。
追追記
// F3.prototype = new F2(); の場合
>>> f3.__proto__
Object c=F3.prototype.c b=F2.prototype.b a=F.prototype.a
>>> f3.__proto__.toSource()
"({c:"F3.prototype.c"})"
>>> dir(f3.__proto__)
a "F.prototype.a"
b "F2.prototype.b"
c "F3.prototype.c"
d "F.prototype.d"
// F3.prototype = F2.prototype; の場合
>>> f3.__proto__
Object a=F.prototype.a b=F2.prototype.b c=F3.prototype.c
>>> f3.__proto__.toSource()
"({a:"F.prototype.a", b:"F2.prototype.b", c:"F3.prototype.c", d:"F.prototype.d"})"
>>> dir(f3.__proto__)
a "F.prototype.a"
b "F2.prototype.b"
c "F3.prototype.c"
d "F.prototype.d"
f3.__proto__.toSource()あたりの結果が違う。
たまたま動くだけで、意図されたものではないってことかな。