prototype.js の Class 拡張試案 (2): 継承とスーパークラスへのアクセス (Update:20060301)

前回 id:reinyannyan:20051116:p1 の内容に幾つか問題点がありましたので、修正や解決策を新しいページにてお知らせします (今後も、新たに問題が発現すればこちらに追記したいと思います)。

Class.extend

まず、新しいクラス継承の方法として Class.extend というメソッドを導入しましたが、これを改めて提示しておきます:

Class.extend = function (object)
{
    var newobj = new this;
    (Object.extend(newobj, object)).__super__ = this.prototype;
    return newobj;
};

変更点は特に無いんですが、new によってインスタンスを作成する関係上 (クローンでは prototype chain が壊れてしまう)、具象クラスの継承の場合、initialize が引数無しで呼び出されてしまうという点が問題となる可能性があります。

initialize の側で、引数無しでもエラーが出ないように配慮すれば良いんですが、そもそも継承の際に初期化を行う仕様に問題があると考えました。以下はこの考えに基づく変更です。

Class.concrete, this.SUPER

ということで、スーパークラスの initialize も含めて、Class.initialize というメソッドを通して初期化を行うように変更します。

Class.concrete = function ()
{
    var klass = function ()
    {
	Object.extend(this, Class.Methods);
	Class.initialize(this, null, arguments);    // <- ココ
    };
    klass.extend = this.extend;
    return klass;
};

Class.Methods.SUPER = function ()
{
    Class.initialize(this, this.__super__, arguments);	// <- ココ
};

Class.initialize

上述の通り、エラー処理のためのラッパーの意味もあるんですが、前回、スーパークラスの initialize の呼び出し方が間違っていました (親クラスが更に this.SUPER() を呼んでいた場合、スタック・オーバーフローになる)。なので、それも同時に解決してみます。

Class.initialize: function (self, superclass, args)
{
    // `self' は常に作成されるインスタンスを指す
    if (superclass)
	// 次に `this.SUPER()' が呼ばれた時、上のクラスを指すように
	self.__super__ = superclass.__super__;

    try
    {
	(superclass || self).initialize.apply(self, args);
    }
    catch (e)
    {
    }

    // 元に戻す
    if (superclass)
	self.__super__ = superclass;
};

これで、this.SUPER() は理論的には際限なく上のクラスを辿って行けるようになると思います。

注意点として、Java との比較をしておきましょう。Java では super を明記しなくても親クラスのコンストラクタが呼ばれます。それに対し、この JavaScript 版では明示的に this.SUPER() としないと呼ばれません (this.SUPER() があるかどうかをコードレベルでチェックするのが嫌だったからです)。

this.SUPER [20051228]

現在呼ばれているメソッドの名前を得る方法が分かりましたので、this.SUPER() を Ruby のように、どのメソッドででも使えるようにしてみます。

関数内部で arguments.callee を参照すると、現在呼ばれている関数 (への参照) を得ることができます。これと、呼び出し元を辿る caller とを組み合わせることで実現します。

SUPER が呼び出される度に、該当する先祖クラスの中でメソッド名を探さなければならない、という点にご留意ください (this.__class__ はこのための一時変数です)。

Class.get_method = function (klass, args)
{
    var _callee = args.caller.callee;
    for (var method in klass)
	if (_callee == klass[method])
	    return method;
    return null;
};
Class.call_super: function (superclass, self, method, args)
{
    if (!(superclass && superclass[method]))
	return;

    // さらに親クラスで this.SUPER() が呼ばれた場合に備える
    self.__class__ = superclass;
    self.__super__ = superclass.__super__;

    // 必要無いかも? > 例外処理
    try
    {
	superclass[method].apply(self, args);
    }
    catch (e)
    {
    }

    // 元に戻す
    delete self.__class__;
    self.__super__ = superclass;
};

Class.Methods.SUPER = function ()
{
    var method = Class.get_method((this.__class__ || this), arguments);
    Class.call_super(this.__super__, this, method, arguments);
}

役に立たないサンプル・コードです:

var A = Class.create();
A.prototype =
{
    initialize: function ()
    {
	this.value = 'Hel';
    },
    hello: function ()
    {
	print(this.value);
    }
};

var B = Class.create();
B.prototype = A.extend(
{
    initialize: function ()
    {
	this.SUPER();
	this.value += 'lo,';
    }
});

var C = Class.create();
C.prototype = B.extend(
{
    initialize: function ()
    {
	this.SUPER();
	this.value += ' wo';
    }
});

var D = Class.create();
D.prototype = C.extend(
{
    initialize: function ()
    {
	this.SUPER();
	this.value += 'rld!';
    }
});

(new D).hello();

プリンタが Hello, world! を打ち出すか、ブラウザが表示するかはあなたの print の実装次第です (笑)。

Class.abstract, Class.concrete [20060301]

インスタンスのプロトタイプを調べてみて、Class.Methods のメソッドが含まれているのはおかしいんじゃないかと思い、修正してみました:

var Class =
{
    abstract: function ()
    {
	var klass = function (){};
	klass.extend = Class.extend;
	return klass;
    },
    concrete: function ()
    {
	var klass = function ()
	{
	    if (!(arguments.caller && arguments.caller.callee == Class.extend))
	    {
		this.__class__ = arguments.callee.prototype;
		Object.extend(this, Class.Methods);
		Class.initialize(this, arguments);
	    }
	};
	klass.extend = Class.extend;
	return klass;
    },

修正した点は

  1. Class.Methods はサブクラスのインスタンス作成時にインクルードされるため抽象クラス (スーパークラス) ではインクルードしない
  2. 具象クラスをさらに拡張する際に初期化メソッドが呼ばれないようにする

のニ点です。

ややこしくなってきたので、こちらでコード全体もアップしてみました。どうぞご覧下さい。

その他

rss に反映されないようなので、今後は新たな事項については個別にエントリーを設けることにします。

id:reinyannyan:20060308:p2 クラス定義方法の仕様変更について
id:reinyannyan:20060312:p1 『Mozilla 系で arguments.caller が非推奨となっていた件』に関する修正