prototype.js と「プロトタイプ」

これまで私が prototype.js の拡張を行ってきた中で、JavaScript という言語に対する姿勢について明確に述べたことは無かったかも知れません (英語版では少し触れたんですが)。なので、いささか唐突ですが、ここで改めて (かつ少々乱暴に) 表してみようと思います:

「JS が基づいているところの『プロトタイプ・ベース』という OOP の方法論について、私はほとんど無知だし、知ろうとも思っていない」

というようなものになります。

ついでに言いますと、私の prototype.js の拡張は、元々 Ruby に強く影響されている prototype.js を、さらに Ruby 的な方向へ (したがってクラス・ベース的な方向へ) 押し進めるのが目的になっています。

つまり、「プロトタイプ・ベース」という概念をできるだけ意識せずにすむようにしたい、という思いが少なからずあるわけです (余談ですが、その意味において id:reinyannyan:20060308:p2 でご紹介した Class.create() の仕様変更案は、prototype.js にとって重要な一歩になるのではないかと思っています)。


そんな、「プロトタイプ・ベース」と相反する立場を取ってきた私にとって、「プロトタイプ」への理解を促すための良い題材 (後述) がありましたので、この機会に一度ちゃんと扱っておきたいと考えました。

以下、便宜的にこの記事で使用する用語を定義しておきます。

  • クラス: オブジェクトのコンストラクタとして使用される関数オブジェクト
  • プロトタイプ: クラスの "prototype" プロパティ; クラス定義

JavaScript の継承モデル

『Ruby の kind_of? の実装』で書いたことの繰り返しになりますが、改めて示しますと、JS における継承とは一般に:

  1. 「サブクラスのプロトタイプ」に「スーパークラスインスタンス」を代入する
  2. 「サブクラスのプロトタイプ」に変更を加える

という二段階の手続きを指します。

コードで示すとこんな感じです:

// クラス定義
function Superclass()
{
    // インスタンス・メソッド定義
    Superclass.prototype.method = function (){...};

    /* NOTE:
     * this.method = ... と "this" 指定子を使って直接インスタンスに対して定義
     * しても良いんですが、メモリー効率上の理由から "prototype" プロパティを通
     * じて定義すべきです (ここではこの定義の仕方を指して「クラス定義」として
     * います)。
     *
     * prototype.js においては、インスタンス変数は "prototype" では定義せず、
     * 初期化メソッドの中で定義するスタイルを取ります。
     */
    ...
}

// サブクラス宣言
function Subclass()
{
}
// 継承
Subclass.prototype = new Superclass;
Subclass.prototype.mymethod = function (){...};
...

このように、JS においてプロトタイプとは、クラス・ベース言語におけるクラスに相当するものであるとともに、継承の仕組みそのものを提供するものである、と言うことができます。

プロトタイプ・チェイン

ここでちょっと実験をしてみましょう。

function A()
{
    A.prototype.hello = function()
    {
	alert('Hello!');
    }
}

function B()
{
}
B.prototype = new A;

A.prototype.hello = function ()
{
    alert('Guten Tag!');
};

alert(B.prototype.hello);

A から B へ継承を行った後で、スーパークラス A の "hello()" の定義を書き換えています。この場合、B の "hello()" はどうなっているでしょうか?

答えは (実は私も今回初めて知ったんですが) 書き換えられた「後」の定義なんですね。「書き換え」でなく「追加」を行った場合でも、同様にサブクラスで利用できるようになります。

このように、スーパークラスでの状態の変化が即座に、自動的にサブクラスに反映 (伝播) されるという点がプロトタイプ・ベース言語の大きな特徴だということになるでしょう (反映されない言語もあるそうです)。

そして、この「伝播」がなぜ可能かと言うと、継承関係にあるプロトタイプ同士、またインスタンスとプロトタイプとの間が「プロトタイプ・チェイン」と呼ばれる参照リンクで結ばれているためなんですね。

ECMA262 によると、このリンクは「暗黙的な参照」であるとされています:

ECMAScript supports prototype-based inheritance. Every constructor has an associated prototype, and every object created by that constructor has an implicit reference to the prototype (called the object's prototype) associated with its constructor. Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain.

つまり、インスタンス作成というプロセス:

instance = new Superclass;

において暗黙的にインスタンスとプロトタイプの間に結ばれるプロトタイプ・チェインを、クラス同士において実現する (即ち、継承関係を実現する) のが、上述のモデルでのこの式である:

Subclass.prototype = new Superclass;

ということになります。

ちなみに、「暗黙的」とされるこのチェインですが、Mozilla 系 JS においては "__proto__" という特別なインスタンス変数として明示的に参照できるようになっています:
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Guide:Property_Inheritance_Revisited:Determining_Instance_Relationships

(なお、私の prototype.js の拡張では同じことを表現するのに "__class__" と "__super__" という二つの変数を使っています。"__proto__" への一本化を目指すべきかも知れません)

prototype.js と「プロトタイプ」

最後に、このエントリーを書くきっかけを (勝手に) 頂きました、id:amachang さんのこの記事『prototype.jsはプロトタイプと言う割りにプロトタイプチェーンがつながらない。』について、コメントを加えさせてください。

一点目は、件の記事で既に指摘されていることの繰り返しなんですが、「プロトタイプチェーンがつながらない」として示されているコード例が、上述の継承モデルのうち、「スーパークラスインスタンスを代入する」という点を満たしていない、ということです。

概念的に説明しますと、

Subclass.prototype = (new Superclass) + サブクラス定義;

とすべき所が

Subclass.prototype = (Superclass.prototype の「浅い」コピー) + サブクラス定義;

となってしまっているんです。プロトタイプ・チェインが生じないことがお分かりでしょうか?

prototype.js 自体のコード中でも、以下のように

Subclass.prototype = Object.extend(new Superclass(), {
  // サブクラス定義
});

ちゃんとインスタンスを生成して継承が行われています。

したがって、この「正しい」継承の仕方に則らないやり方での「つながらない」との指摘は当たらないのではないか、というのが私の意見です。


二点目は、ではなぜインスタンスを使いたくないのかということに関する次の指摘:

コンストラクタの引数はどうなるのか?他人が書いたクラスのプロトタイプを作るための適切な引数を導き出せるのか。

この点については全く同感で、私も id:reinyannyan:20051116:p1 等で試行錯誤を繰り返しました。

その結果、「継承の場合は初期化メソッドを呼ばない」という仕組みを作ることでこの問題を回避することができています。詳細については、現在の prototype.js の拡張コードの、Class.create 辺りを見ていただければと思います。


参考文献:
http://en.wikipedia.org/wiki/Prototype-based_programming