全然理解出来てなかったので調べてみた。
function f () {
// Class
}
function F () {
// SubClass
}
という状況で継承させる時、サイ本などでは次のようにする。
F.prototype = Object.create(f.prototype);
F.prototype.constructor = F;
などとしていることが多いが、ウェッブサイトなどでは次のようにやっていることが多 い。
F.prototype = new f();
この二つのやり方を比較してみた。
最近、テスト駆動JavaScriptという本を読んでいるので、 JsTestDriverを使った、テストによる方法で実験的に試してみたい。
次のようなJavaScriptを作る。
// 基底クラス
function f () {
this.f_has = "f_has";
}
// 基底クラスのprototypeを設定
f.prototype.f_proto = "f_proto";
// サイ本の手法で継承
function F () {
this.F_has = "F_has";
}
F.prototype = Object.create(f.prototype);
F.prototype.constructor = F;
// ウェッブの手法で継承
function G () {
this.G_has = "G_has";
}
G.prototype = new f();
次のテストを試す。
TestCase("inherit test", {
setUp : function () {
this.F = new F();
this.G = new G();
},
"test instance" : function () {
assertTrue(this.F instanceof F);
assertTrue(this.F instanceof f);
assertTrue(this.F instanceof Object);
assertTrue(this.G instanceof G);
assertTrue(this.G instanceof f);
assertTrue(this.G instanceof Object);
}
});
これは普通に通るので、両方なんとなく上手くいっているように見える。
次のテストではサイ本式とウェッブ式で差が出る。
"test check F, G f_has" : function () {
// サイ本のやり方だと、スーパークラスのプロパティが無い
assertUndefined(this.F.f_has);
// ウェッブの方はある
assertEquals("f_has", this.G.f_has);
}
まあそれもそのはずで、サイ本式はf()
のコンストラクタを実行していないので、当然といえばそう。
試しにf()
を改造してみる。
function f () {
console.log("done!");
// ...
}
それでまたテストを走らせると、一度だけ done! が出現する。
サイ本式で、継承先のコンストラクタで継承元のコンストラクタを、
実行するにはF()
を次のようにすればよい。
function F () {
f.apply(this);
// ...
}
スーパークラスの名前をいちいち使っているあたりが汚ないが、 この時点でウェッブ式よりもよいように見える。 スーパークラスのコンストラクタをサブクラスで呼んでいるので、 スーパークラスのコンストラクタに引数を渡せる。
function f (a) {
this.a = a;
}
function F () {
f.apply(this, arguments);
}
F.prototype = Object.create(f.prototype);
F.prototype.constructor = F;
この中ではクラスと言ってきたが、そもそもJavaScriptにはクラスなどない。 これらは何をしてきたのかといえば、プロトタイプチェーンで似たようなものを作ったという感じだと思う。 いままで出現したオブジェクトは、
f
F
G
という二つの関数オブジェクトがある。
そして、テストの中ではnew
演算子を用いて、二つのオブジェクトを生産した。
そもそも、new
演算子は関数を呼び出すときに、その関数内のthis
を、
左辺のオブジェクトへの参照にするという働きをすると思う。
つまり、次にある二つの文はだいたい同じ意味を持つ。
var hoge = new Hoge(arg1, arg2);
var hoge = {};
Hoge.call(hoge, arg1, arg2);
要するに、new
演算子はあるオブジェクトに関数オブジェクトを適応したように振る舞う。
こんな手法で、あたかもクラスらしいものを生成している。
最初のコードを少し変えると、さらにウェッブ式は不味いことが見えてくると思う。
// ユニークな数字this.uidを作るコンストラクタを持つ
var f = (function () {
var uid = 0;
return function () {
this.uid = uid;
uid++;
};
}())
// サイ
function F () {
f.apply(this);
}
F.prototype = Object.create(f.prototype);
F.prototype.constructor = F;
// ウェッブ
function G () { }
G.prototype = new f();
テストではさらにここで、F
, G
のインスタンスを四つ生成している。
まあ、テストするまでもなくヤバいような気はするが、まあやってみる。
TestCase("inherit test 2", {
setUp : function () {
this.F0 = new F();
this.F1 = new F();
this.G0 = new G();
this.G1 = new G();
},
"test uid of F" : function () {
assertNotEquals(this.F0.uid, this.F1.uid);
},
"test uid of G" : function () {
assertNotEquals(this.G0.uid, this.G1.uid);
}
});
うまくいけばいいねー。と思ったけど無理。
test uid of G で失敗する。
というわけでconsole.log
で調べてみる。
"test uid of F" : function () {
console.log("F0 : " + this.F0.uid);
console.log("F1 : " + this.F1.uid);
assertNotEquals(this.F0.uid, this.F1.uid);
},
"test uid of G" : function () {
console.log("G0 : " + this.G0.uid);
console.log("G1 : " + this.G1.uid);
assertNotEquals(this.G0.uid, this.G1.uid);
}
inherit test 2.test uid of F passed (0.00 ms)
[LOG] F0 : 9
[LOG] F1 : 10
inherit test 2.test uid of G passed (0.00 ms)
[LOG] G0 : 0
[LOG] G1 : 0
まあそうなるよな。
// サイ
function F () {
f.apply(this);
}
F.prototype = Object.create(f.prototype);
F.prototype.constructor = F;
// ウェッブ
function G () { }
G.prototype = new f();
ここで、サイ本の方はインスタンスが生成される度に、その継承元のf
のインスタンスを生成しているが、
ウェッブで見る方はG.prototype
に一発放り込んで終わり。
だからthis.uid
も一度しか生成されないという訳ですね。
僕はサイ本の方を採用すると思うけど、 ウェッブ式も実は何か利点があるのかもしれない。
この二つの方法による違いを指摘してくだり、調べるきっかけを下さったgfxさんに感謝します。
コメントがあったのでここにまとめる。 こんなふざけた記事を読んでくださるとは、感謝。
サイ本ではObject.create()
を直接使わず、inherit()
という関数を噛ませてます。
function inherit (p) {
if (Object.create) {
return Object.create(p);
}
function f() {};
f.prototype = p;
return new f();
}
function f () { }
function F () { }
F.prototype = inherit(f.prototype);
F.prototype.constructor = F;
と、このような関数。
最後の三行でObject.create()
を自力で実装してますね。
これは、JavaScript The Good Partsでも
やっていた伝統的な手法。
まあただ、今時Object.create()
の無いブラウザなんて相手にしたくない。
Google 製の JS ライブラリが↑の問題をきれいに解決してくれていますので、参考になると思います:
https://github.com/google/closure-library/blob/master/closure/goog/base.js#L1550