Bookmark this on Hatena Bookmark
Hatena Bookmark - enchant.jsのクラス継承とJavaScriptのクラス継承のちょっとした違い
Share on Facebook
Post to Google Buzz
Bookmark this on Yahoo Bookmark
Bookmark this on Livedoor Clip
Share on FriendFeed

そろそろWebGLにも対応したバージョンがリリースされそうなenchant.js。みんなやってるかな?
ところでTwitterで気になる発言を目にしました。

enchant.jsのクラス継承のところでつまづいている、ということだったので、今日はこの微妙にややこしい問題を扱ってみようと思います。

まず、JavaScriptのクラス継承は、プロトタイプベース継承と言って、ちょっと特殊です。
JavaScriptのクラス継承については@ITのこの記事が非常に解り易くまとまっています。


しかしこのプロトタイプベース継承という考え方が、そもそもC++やRuby、Javaなど、他のクラスベース継承を採用した言語から入門した人にはちょっとわかりにくいのです。

すごく大雑把に説明すると、プロトタイプベースというのは、.prototypeに指定された要素が常にコピーされるというやり方。

これを説明するために、まずはJava(クラスベース)のクラス定義をおさらいしましょう。

//Javaの場合
class A {
  int x;
  public A(){
    x=1;
  }
  public  int method(){
    System.out.println(x);
  }
}

//クラスを使うとき
A a = new A();
a.method();

みたいな感じになります。

JavaScriptは、Javaと名前は似てますが、クラスの考え方がプロトタイプベースと、全く異なる考え方になっています。

同じことをするクラスをJavaScript(プロトタイプベース)で書くと以下のようになります。

//JavaScriptの場合
A = function(){
    this.x=1;
};
A.prototype.method = function(){
	document.write(this.x);
}

//クラスを使うとき
var a = new A();
a.method();

Javaと比較するとなんじゃこりゃ、という感じです。
名前は似てるのに違い過ぎて衝撃を受けます。

しかしもっとひどいのは、継承をした場合。
Javaで継承をすると以下のようなコードになります。

//Javaの場合
class A {
  int x;
  public A(){
    x=1;
  }
  public  int method(){
    System.out.println(x);
  }
}

class B extends A {
  int y;
  public B(){
    super(); //親クラスのコンストラクタを呼ぶ
    y=100;
  }
  public  int anotherMethod(){
    System.out.println(x*y);
  }
}

//継承したクラスを使うとき
B b = new B();
b.method(); //親クラスのメソッド呼び出し
b.anotherMethod(); //継承したクラスのメソッド呼び出し

extendsというキーワードで継承するクラスを指定するわけです。
AとBのクラス定義がハッキリ別れているので、パッと見た感じ解り易いです。

ところがJavaScriptの継承はこれに慣れているとかなり不思議なやりかたになっています。

A = function(){
    this.x=1;
};
A.prototype.method = function(){
	document.write(this.x);
}

B = function(){
    A.call(this); //親クラスのコンストラクタを呼ぶ
    this.y=100;
}
B.prototype = new A(); //ここで継承している
B.prototype.anotherMethod = function(){
	document.write("<BR>"+this.x*this.y);
}

var b = new B();
b.method();
b.anotherMethod();

継承をしている部分は、B.prototypeになんと生成したA(継承元クラス)のオブジェクトを突っ込んでいます。

というのは、JavaScriptの場合、コンパイル言語ではなくインタプリタ言語である、という特性が大きく活かされた継承モデルになっているのです。

JavaScriptでは、prototypeプロパティに指定されたメンバーは、new演算子で新しくインスタンス生成する際に自動的にコピーされます。

そして継承とは、親クラスの内容をまるごと子クラスがコピーする(厳密にはコピーされるのではなく、暗黙的に参照される。詳しくは@ITの記事参照)、というのがJavaScriptの基本的なやり方なのです。

これは処理系(JavaScriptの)を実装する側からすると、非常に簡単にクラス継承を実現できることになります。なにしろ単にコピーするだけでいいんですから。

クラスを定義したり継承したりするための特殊な構文も必要ありません。

その代わり、デメリットとして、そもそもオブジェクト指向が持っている重要な機能としての隠蔽化の機能が全く実現できない(全てのメンバーがpublicであること)ということが挙げられます。

これは巨大で堅牢なプログラムを書くには大きな障害になります。
誰でもいつでも好きなように言語全体の構造を変更できてしまうのです。

たとえば、JavaScriptでは組み込みクラスの配列がありますが、好きなタイミングで配列の機能そのものを追加したり変更したりすることができます。

Array.prototype.remove=function(obj){ //配列の任意の要素を削除するメソッドremoveを追加
	for(i = 0; i < this.length; i++){
 		if(this[i] === obj){
 			this.splice(i,1);
		}
	}
}

var hoge = [1,2,3,4];

hoge.remove(3);

for(i=0;i<hoge.length;i++)
	document.write(hoge[i]+','); //配列の中身を表示

上記は配列の任意の要素を削除するメソッドremoveを組み込みクラスに追加した例です。

しかし、JavaScriptはいったいなぜ、プロトタイプ継承なんていう特殊なやりかたを採用したのでしょうか。

それにはいろいろな理由が考えられます。
が、ひとつ言えることは、コンパクトなプログラムを手早く書くには、プロトタイプ継承は慣れれば便利だということです。

好きなタイミングで好きなメンバーを追加したり、オブジェクトごとにメンバーの内容を変更したりといったことが容易です。

これは、Javaなどの一般的なクラスベース言語では第一級関数(関数を変数と同格に扱うこと)がサポートされていないのに対し、JavaScriptでは第一級関数をサポートしているということが大きいと思います。

そのぶん、これを駆使してプログラムを書くと、コードはかなり混乱しやすくなります。
そのかわり、慣れると非常に素早く目的とするプログラムを書くことが出来るようになります。

だから複数人でJavaScriptのプログラム開発をやるときは注意が必要です。
名前空間がバッティングしたり、触って欲しくないプロパティを触られてしまうリスクも発生します。

ただ、ゲームプログラミングにおいては、実はクラスベース継承の方が基本的に向いています。
そこでenchant.jsでは、クラスベース継承を擬似的にサポートするためのユーティリティ関数として、Class.createが用意されています。

Class.createの使い方は簡単です。

enchant(); //enchant.jsを使うおまじない

A = Class.create({       // Class.createでのクラス宣言
    initialize:function(){  //コンストラクタ
        this.x=1;
    },
    method:function(){
        document.write(this.x);
    }
});

//使う場合は通常のJavaScriptと同じ
var a = new A();
a.method();

ミソは、Class.createにリテラル表現のオブジェクトを渡しているところです。

こうすると、一見、Javaのクラス宣言のようなクラス定義を表現できます。
継承する場合のコードは以下のような感じに成ります

enchant();。

A = Class.create({
    initialize:function(){
        this.x=1;
    },
    method:function(){
        document.write(this.x);
    }
});

B = Class.create(A,{   //Aを継承
    initialize:function(){
	A.call(this);  // Aのコンストラクタを呼び出し
        this.y=100;
    },
    anotherMethod:function(){
        document.write(this.x*this.y);
    }
});

var b = new B();
b.method();
b.anotherMethod();

こうすると、prototypeを使って継承するよりは見た目がスッキリします。
クラスベースの継承に慣れたプログラマにとっても、JavaScriptのプロトタイプ継承をそのまま使うよりは読み易いコードになります。

そのうえ、プロトタイプベースの機能も使えるので、工夫次第でJavaでは表現するのに手間取るような過激なコードを書くこともできます。

例えばインスタンスごとに同じ名前で全く違う動作をするメソッドを追加したりするなどです。

enchant();

A = Class.create({
    initialize:function(){
        this.x=1;
    },
    method:function(){
        document.write(this.x);
    }
});

var m = [];
for(i=0;i<10;i++){
    var obj = new A();
    m.push(obj);
    switch( i % 4 ){
        case 0:
	    obj.method=function(){ //method()を全く違う内容に書き換えている
	        document.write("<BR>");
	    };
	    break;
        case 1:
	    obj.method=function(){ //method()を全く違う内容に書き換えている
	        document.write("booe");
	    };
        case 2:
	    obj.x=2
	    break;
    }
}

for(i=0;i<m.length;i++)
    m[i].method();

こんなことができます。
これをゲームプログラミングに応用すると、敵キャラの基本的なクラスをClass.createで定義しておき、雑魚キャラの細かい動きのところだけ同じメソッド名で違う内容にする、といったことが可能です。

呼び出す時は配列に対して一括で同じメソッド名を呼ぶだけなので、プログラミングがし易くなります。

もちろん、クラスベースの言語でも、全ての敵キャラのパターンにあわせて別のクラスを定義したりすれば実現できますが、ゲームで使う雑魚キャラの動きなどはそれほど大きく違うこともないので、本当に違う部分「だけ」を定義するならプロトタイプベースのメリットを活かした方が得です。

こんなふうに、メソッドやプロパティの名前が同じなら、同じ動作をする「はずだ」というプログラミング手法を「ダックタイピング」と呼んだりします。

この「ダックタイピング」が自由に使えるようになると、それができない言語に比べて何倍も効率的なコードを書くことが出来るのです。

今回はちょっと難しい話をしてしまったかもしれません。
どうですか?解っていただけたでしょうか。