Skip to content

Instantly share code, notes, and snippets.

@y-yu
Created August 9, 2012 06:57
Show Gist options
  • Save y-yu/3301790 to your computer and use it in GitHub Desktop.
Save y-yu/3301790 to your computer and use it in GitHub Desktop.
JavaScriptの継承について

JavaScriptの継承について

全然理解出来てなかったので調べてみた。

経緯

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 演算子

そもそも、new演算子は関数を呼び出すときに、その関数内のthisを、 左辺のオブジェクトへの参照にするという働きをすると思う。 つまり、次にある二つの文はだいたい同じ意味を持つ。

var hoge = new Hoge(arg1, arg2);

var hoge = {};
Hoge.call(hoge, arg1, arg2);

要するに、new演算子はあるオブジェクトに関数オブジェクトを適応したように振る舞う。 こんな手法で、あたかもクラスらしいものを生成している。

F.prototype, G.prototype

最初のコードを少し変えると、さらにウェッブ式は不味いことが見えてくると思う。

// ユニークな数字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()って新しいから古いブラウザはちょっとね……

サイ本では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()の無いブラウザなんて相手にしたくない。

@Kuniwak
Copy link

Kuniwak commented Aug 11, 2014

スーパークラスの名前をいちいち使っているあたりが汚ないが、 この時点でウェッブ式よりもよいように見える。 スーパークラスのコンストラクタをサブクラスで呼んでいるので、 スーパークラスのコンストラクタに引数を渡せる。

Google 製の JS ライブラリが↑の問題をきれいに解決してくれていますので、参考になると思います:

https://github.com/google/closure-library/blob/master/closure/goog/base.js#L1550

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment