JavaScriptの継承関数を考える

今更ながらJavaScriptの継承について再考してみました。JavaScriptの継承については、だいぶ前から何度も考えているのですが、個人的にコレといった決定版がない感じ。

最近、個人的によく使っている継承パターンでした。

var extend = function(obj, func) {
  var F = function() {};
  F.prototype = obj;
  var f = new F();
  if (func) func.call(f);
  return f;
}

JavaScript継承パターンまとめ - Thousand Yearsにあるクローンパターンに、クロージャを併用しています。

継承するのはオブジェクト(インスタンス)です。function(クラス)ではありません*1

以下のようにして使えます。

Hoge = extend(Object, function() {
  var localVariable = 100;
  this.print = function() { console.log(localVariable); }
});
Huga = extend(Hoge, function() {
  this.print = function() { console.log("override!"); }
});
hoge = extend(Hoge);
hoge.print();
huga = extend(Huga);
huga.print();

クロージャを利用することでローカル変数が使えるようになるので、いいんでないかなーと思っています。ただ、superが使えないです。ん〜何か良い方法が思いついたらいいなぁ。

ちょっと考えてみた(2012/7/28*2

結果、以下が良い感じじゃないかという気がしてきた。忘れないうちにメモ。

var Class = {
	__new__: function() {
		function F() {}
		F.prototype = this;
		return new F();
	},
	__extend__: function(c) {
		var f = this.__new__();
		f.__super__ = this;
		c.call(f);
		return f;
	}
};

var Foo = Class.__extend__(function() {
	var name = "Foo";
	this.doSomething = function() {
		console.log(name);
	};
});

var Bar = Foo.__extend__(function() {
	var name = "Bar";
	this.doSomething = function() {
		this.__super__.doSomething();
		console.log(name);
	};
});

var Hoge = Bar.__extend__(function() {
	var name = "Hoge";
	this.doSomething = function() {
		this.__super__.doSomething();
		console.log(name);
	};
});

Hoge.__new__().doSomething();

しかし__new__でコンストラクタの引数を渡せないのはやっぱり微妙な気がしてきた。

__new__を出来るようにしたバージョン(2012/7/29)

var Class = {
	__init__: function() {},
	__clone__: function() {
		function F() {}
		F.prototype = this;
		return new F();
	},
	__new__: function() {
		var f = this.__clone__();
		f.__init__.apply(f, arguments);
		return f;
	},
	__extend__: function(c) {
		var f = this.__clone__();
		f.__super__ = this;
		c.call(f);
		return f;
	}
};

何だか微妙。しかもここにきて、そもそもクロージャ使うのはあまりうまくないように感じた。例えば下のような時に

var Foo = Class.__extend__(function() {
	var name = "Foo";
	this.__init__ = function(x) {
		if (x)
			name = x;
	};
	this.test = function() {
		console.log(name);
	};
});

var Bar = Foo.__extend__(function() {
	this.__init__ = function(x) {
		this.__super__.__init__(x ? x : "Bar");
	};
});

var foo = Foo.__new__();
foo.test(); // Foo
foo = Foo.__new__("FooFoo");
foo.test(); // FooFoo
var bar = Bar.__new__();
bar.test(); // Bar
foo.test(); // Bar

やっぱり他に影響してしまうのは問題な気がします。

そもそも何故、決定版が無いように思えていたのか。

元々、一番オーソドックス(?)なprototypeにnewするパターン*3を使っていたのですが、一々prototype書くのが面倒なのが嫌でした。ということで、下のような感じにすれば問題ないのではないかと思えてきました。

function extend(s, c, m) {
	function f() {}
	f.prototype = s.prototype;
	c.prototype = new f();
	c.prototype.__super__ = s.prototype;
	c.prototype.__super__.constructor = s;
	c.prototype.constructor = c;
	for (var i in m)
		c.prototype[i] = m[i];
	return c;
};

第3引数にメンバを指定できるようにしただけです。とりあえずコレで様子見します。

更に改変(2012/7/31)

少し上記を使ってみましたが、意外とコンストラクタのいらない時があり、第二引数に無名関数を指定しないといけないのが面倒だったのと、あとコンストラクタだけ別個というのも何だか気になったので若干変更。

function extend(s, m) {
	function f() {}
	f.prototype = s.prototype;
	var c = m.constructor ? m.constructor
			: m.constructor = function() {};
	c.prototype = new f();
	c.prototype.__super__ = s.prototype;
	c.prototype.__super__.constructor = s;
	for (var i in m)
		c.prototype[i] = m[i];
	return c;
};

var Foo = extend(Object, {
	constructor: function() {
		this.name = "Foo";
	},
	test: function() {
		console.log(this.name);
	}
});

var Bar = extend(Foo, {
	constructor: function() {
		this.__super__.constructor();
		this.name = "Bar";
	},
	test: function() {
		this.__super__.test();
		console.log(this.name);
	}
});

new Foo().test(); // Foo
new Bar().test(); // Foo Bar

というかこれYUIとほぼ一緒でした…
yui3/src/oop/js/oop.js at master · yui/yui3 · GitHub

↑はダメです。

これだと、Objectを拡張すると、Object.prototype.constructor = Objectなので、Objectのプロトタイプが汚されます。
ということで、折衷案。

function extend(s, c_, m) {
	function f() {}
	f.prototype = s.prototype;
	c = c_ ? c_ : function() {};
	c.prototype = new f();
	c.prototype.__super__ = s.prototype;
	c.prototype.__super__.constructor = s;
	c.prototype.constructor = c;
	for (var i in m)
		c.prototype[i] = m[i];
	return c;
};

結論として、YUIのextendを使うべきだと思いました。

*1:もちろんobjに関数を渡すことはできますが、生成されるのがオブジェクトなので基本的にはオブジェクト単位での拡張になります。

*2:オリンピック開会式1時間前

*3:あのサイトで言えばextend