[JavaScript] クラスの継承

2012 年 5 月 11 日 Categories: JavaScript | Tags: ,

	var message = "";

	// SuperClassの定義
	function SuperClass() {
		message += "SuperClass.prototype.constructor\n";
		this.prop = 1;
	}
	
	SuperClass.prototype.doMethod = function () {
		message += "SuperClass.prototype.doMethod\n";
	}
	
	SuperClass.prototype.getProp = function () {
		return this.prop;
	}


	// SubClassの定義
	function SubClass() {
		message += "SubClass.prototype.constructor\n";
		this.prop = 2;
	}
	
	SubClass.prototype = new SuperClass; // (1)
	
	SuperClass.prototype.getProp = function () {
		return this.prop;
	}
	
	new SubClass;
	
	alert(message);

JavaScriptでクラスの継承を行なう場合、prototypeにスーパークラスのインスタンスを代入するのが一般的。

しかし上の例では、messageは「SuperClass.prototype.constructor」と表示され、つまりスーパークラスのコンストラクタしか呼ばれない。

これは、(1)「SubClass.prototype = new SuperClass;」の際に、サブクラスのコンストラクタ(SubClass.prototype.constructor)までスーパークラスのそれ(SuperClass.prototype.constructor)で上書きされてしまうため。

つまりnew SubClassがnew SuperClassと同じになってしまっている。

そこで以下のようにして、継承後にサブクラスのコンストラクタを元に戻す。

var originalConstructor = SubClass.prototype.constructor;
SubClass.prototype = new SuperClass;
SubClass.prototype.constructor = originalConstructor;

ここでnew SubClassとすると、今度はサブクラス側のコンストラクタしか呼ばれない。

対処方法は、サブクラスのコンストラクタからスーパークラスのコンストラクタを明示的に呼び出すこと。

	function SubClass() {
		SuperClass.prototype.constructor.call(this);
		
		message += "SubClass.prototype.constructor\n";
		this.prop = 2;
	}

もちろん、コンストラクタの最初に記述する。他の言語でいうsuper()。

「SuperClass.prototype.constructor.call(this);」と記述しているのには訳がある。

「this.constructor();」とした場合、newでコンストラクタを呼び出すと、その内部のthisはインスタンス自身を示すので、無限ループに陥ってしまう。

「SuperClass.prototype.constructor();」とするのは、根本的にprototypeの使い方を誤っている。

JavaScriptの場合、prototypeに登録した関数(メソッド)を上記のように静的に呼び出すと、その内部のthisはインスタンスではなくprototypeを示す。

constructor()で、クラスインスタンスではなくprototypeを操作することになってしまう(くわしくはこちら)。

JavaScriptでは、呼び出した関数がどのオブジェクトに所属するかでthisが変わってしまうので、明示的にスーパークラスのコンストラクタをcall()、もしくはapply()でthisを明確化して呼び出すしかない。

自クラスが継承された場合、コンストラクタがどういう形で呼び出されるか予測がつかない=thisが不確定ので、こういった配慮が必要になる。

同じ問題はスーパークラスのコンストラクタだけでなく各メソッドでも同様なので、たとえ同じクラスのコンストラクタから同じクラスのメソッドを実行する際も、call/apply()でthisを明確にする。

	function SubClass() {
		SuperClass.prototype.constructor.call(this);
		
		message += "SubClass.prototype.constructor\n";
		this.prop = 2;
		SubClass.prototype.doMethod.call(this);
	}

気をつけなければならないのは、各メソッドから別のメソッドを呼ぶ場合には、逆にthisからコールすること。

	SuperClass.prototype.doMethod = function () {
		message += "SuperClass.prototype.doMethod\n";
		message += this.getProp();
	}
	
	SuperClass.prototype.getProp = function () {
		return this.prop;
	}

もし「SuperClass.prototype.getProp.call(this);」とすると、サブクラス側でgetProp()をオーバーライドしても、常にSuperClassのgetProp()が呼び出されてしまう。

var subClass = new SubClass;
subClass.doMethod();

alert(message); // 1

propプロパティの値はSubClassでは2なのに、SuperClassの1が返る。つまり、多態性が働かなくなる。

以上でひととおり完成。

	var message = "--- 定義 ---\n";

	// SuperClassの定義
	function SuperClass() {
		message += "SuperClass.prototype.constructor\n";
		this.prop = 1;
	}
	
	SuperClass.prototype.doMethod = function () {
		message += "SuperClass.prototype.doMethod\n";
		message += "this.prop is " + this.getProp();
	}
	
	SuperClass.prototype.getProp = function () {
		return this.prop;
	}


	// MiddleClassの定義
	function MiddleClass() {
		// super()
		SuperClass.prototype.constructor.call(this);
		
		message += "MiddleClass.prototype.constructor\n";
		this.prop = 2;
	}
	
	// 継承
	var originalConstructor = MiddleClass.prototype.constructor;
	message += "------ MiddleClassへの継承\n";
	MiddleClass.prototype = new SuperClass;
	MiddleClass.prototype.constructor = originalConstructor;
	
	SuperClass.prototype.getProp = function () {
		return this.prop;
	}


	// SubClassの定義
	function SubClass() {
		// super()
		MiddleClass.prototype.constructor.call(this);
		
		message += "SubClass.prototype.constructor\n";
		this.prop = 3;
	}
	
	// 継承
	var originalConstructor = SubClass.prototype.constructor;
	message += "------ SubClassへの継承\n";
	SubClass.prototype = new MiddleClass;
	SubClass.prototype.constructor = originalConstructor;
	
	SubClass.prototype.getProp = function () {
		return this.prop;
	}
	
	message += "--- SubClassのnew ---\n";
	subClass = new SubClass;
    subClass.doMethod();
    
	alert(message);

結果:

— 定義 —
—— MiddleClassへの継承
SuperClass.prototype.constructor
—— SubClassへの継承
SuperClass.prototype.constructor
MiddleClass.prototype.constructor
— SubClassのnew —
SuperClass.prototype.constructor
MiddleClass.prototype.constructor
SubClass.prototype.constructor
SuperClass.prototype.doMethod
this.prop is 3

多段階の継承も、このように問題ない。

JavaScriptでのクラスの継承にはさまざまな方法があるが、おそらくこれがもっともオーソドックスだと思う。