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

(注意: この記事には幾つか問題点があります。次の記事 id:reinyannyan:20051118:p1 で修正しますので、そちらと併せてお読みください。)

prototype.js は、JavaScript というプロトタイプ・ベースのオブジェクト指向言語 (クラスが存在しない = 全てがオブジェクト、という世界) に、クラス・ベース的なクラス設計や継承の仕組みを与えます。

これは画期的な、革命的な出来事と言っても良いと思いますが、少し不満もあります。スーパークラスの定義が全てサブクラスによって上書きされてしまうという点です。

以下は、この問題を何とかしてみようという試みです。

ソリューション 1

オリジナルの Object.extend を見てみると、第一引数に親オブジェクト、第二引数には子オブジェクトを受け取り、子オブジェクトから親オブジェクトに対してメンバーを追加、あるいはコピーする、という方法を取っています。

引用:

Object.extend = function(destination, source) {
  for (property in source) {
    destination[property] = source[property];
  }
  return destination;
}

ところがこの「子 -> 親」というコピー方向だと、「親 -> 子」という継承方向を保存することができません。そこで、逆方向にしてみます。

Object.extend = function(destination, source, subclassing) {
  if (subclassing)  // クラス継承モード
  {
    // destination == superclass, source == subclass
    for (var property in destination)
      // サブクラスに存在しない場合のみ、コピー
      if (source[property] == undefined)
	source[property] = destination[property];
    source.__super__ = destination;   // super が予約語なので
    return source;
  }
  else
  {
    for (property in source) {
      destination[property] = source[property];
    }
    return destination;
  }
}

これで見事、スーパークラスの定義を継承しつつ、スーパークラスへの参照も持ったサブクラスを作ることができます。

ところが、このソリューションには重大な問題があることが分かりました。それは、プロトタイプ・チェインが破れてしまう、という問題です。

つまり、この方法で作ったサブクラスのインスタンスは、そのスーパークラスのクラス階層に属することができなくなってしまうんです (従来の方法では生じなかった問題です)。

ソリューション 2

上の問題を踏まえて、書き換えてみましょう:

Object.extend = function(destination, source, subclassing) {
  if (subclassing)  // クラス継承モード
  {
    var tmpsub = Object.clone(source), tmpsuper = Object.clone(destination);
    // Object.clone の中身は Object.extend({}, object) という感じです

    source = destination;   // prototype chain を壊さないように
    Object.extend(source, tmpsub);

    source.__super__ = tmpsuper;  // destination は代入できない。同じものなので
    return source;
  }
  else
  {
    for (property in source) {
      destination[property] = source[property];
    }
    return destination;
  }
}

これで (1) の問題は解決されるんですが、継承の度にスーパークラスインスタンスのコピーを作成、保持するという点でメモリー消費の問題が気になってきます。

ソリューション 3 (最終解?)

アプローチを変えてみましょう。

このような、クラスの継承専用のメソッドを作ってみます:

Class.extend = function (object)
{
  // 問題: `this' は何を指すでしょうか?
  var newobj = new this;  /* <- 具象クラスを拡張する場合、initialize に引数を
			   * 渡せず、不具合を生じ得る。なので、clone を使うこ
			   * とに (Update: 11/17)
			   * prototype chain が壊れるのでダメでした (苦笑
			   * やっぱり new を使う (11/18)
			   */
  //var newobj = Object.clone(this.prototype);
  Object.extend(newobj, object);
  newobj.__super__ = this.prototype;
  return newobj;
};

そして、id:reinyannyan:20051028:p1 で提案した Class.abstract と Class.concrete を以下のようにします:

Class.abstract = function ()
{
  var klass = function ()
  {
    Object.extend(this, Class.Methods);
  };
  klass.extend = this.extend;
  return klass;
};
Class.concrete = function ()
{
  var klass = function ()
  {
    Object.extend(this, Class.Methods);
    this.initialize.apply(this, arguments);
  };
  klass.extend = this.extend;
  return klass;
};

さて、何がしたいかお分かりでしょうか。そうなんです、クラスの継承方法を変えたんです。実例をお見せしましょう。

var GrandFather = Class.create();
GrandFather.prototype =
{
  initialize: function ()
  {
    // GrandFather
  }
};

var Father = Class.create();
Father.prototype = GrandFather.extend(  // <- ココ注目
{
  initialize: function ()
  {
    // Father
  }
});

var Child = Class.create();
Child.prototype = Father.extend(
{
  initialize: function ()
  {
    // Child
  }
});

// テストしてみてください
function test(obj)
{
  while (obj)
  {
    alert(obj.initialize);
    obj = obj.__super__;
  }
}
test(new Child);

さて、スーパークラスに階層的にアクセスできるようになったところで、さらに、JavaRuby 等の super() にあたるものも作ってみましょう。

Class.Methods.SUPER = function ()
{
  this.__super__.initialize.apply(this, arguments);
};

サブクラスの initialize メソッドの中で this.SUPER(/* 引数あれば普通に */) とすると、スーパークラスの初期化メソッドを呼び出せるようになります。もちろん、生成されたインスタンス変数は呼び出し側のオブジェクトのものとなります*1

結論

上で提示した新たなクラス継承方法によって、JavaScript はまた大きく (言語本来の設計思想に反して) クラス・ベース言語に近づくと言えるでしょう。しかしながら、prototype.js ユーザーに対するインパクトが甚大と思われるため、本家 prototype.js への導入は容易ではないでしょう。この点についてはいずれ作者氏に意見を聞いてみたいと思っております。

また、そこまでする必要があるのか、というご意見もあるかも知れません。それに対しては、「すいません。でも、楽しいじゃないですか!」と申し上げたいと思います。

*1:注意[11/17]: スーパークラスがさらに this.SUPER() を呼び出していると、エラーが生じます。対策考え中