JavaScriptによるオブジェクト指向プログラミング
JavaScriptによるオブジェクト指向プログラミング
はじめに
ここでは,オブジェクト指向風JavaScriptプログラミングスタイルの解説をします.対象とする読者は,JavaScriptの基本的な文法とコンストラクタ関数,prototype についての知識が必要となりますが,オブジェクト指向さえ知っていればとりあえず大丈夫だと思います(とはいえ,JavaScriptユーザの人数に比べたらオブジェクト指向を理解しているユーザってごく少数なんでしょうね).
なお,ここに書いてあるスタイルは,IE4で試しただけなので,問題があるかもしれません.もしお気づきの点などがあれば連絡してください.
クラス定義
コンストラクタとインスタンス変数
function Class() { this.instanceVariable = initValue; }
インスタンスメソッド
function _Class_instanceMethod () { ... } Class.prototype.instanceMethod = _Class_instanceMethod;
クラス変数
Class.classVariable = initValue;
クラスメソッド
function _Class_classMethod() { ... } Class.classMethod = _Class_classMethod;
注釈
メソッド定義ですが,上記の方法ではなく
function Class.prototype.instanceMethod () { ... }
と定義できたほうが名前空間も汚染されずスマートだと思います.これはIE4で動作しますが,Netscapeでは動作しません.それにECMAScriptの規格に合致していないように思われます.
また,
Class.prototype.instanceMethod = function() { ... }
という書き方(無名関数?)をするとIEでもNetscapeでも動作しますが,やはりECMAScriptの規格に合わないでしょう.ECMAScriptでもnew Functionを使って無名関数をつくることはできますが,こうすると非常に煩雑でプログラミングしにくいものになります.以上から,ここでは一番無難な方法を採用しました.
継承
注)このセクションはオブジェクト指向言語としてのJavaScriptというページを参考にさせていただきました.
継承関数 - inherit
function copy_undef_properties(src, dest) { for (var prop in src) { if (typeof(dest[prop]) == "undefined") { dest[prop] = src[prop]; } } } function inherit(subClass, superClass) { copy_undef_properties(superClass.prototype, subClass.prototype); }
copy_undef_propeties という関数を使って prototype オブジェクトのコピーを行っています.なぜ別関数にしたのかは後でわかりますので今は気にしないで下さい.undefinedのものだけに限定したのは,子クラスでオーバーライドされていた場合に間違って親クラスのメソッドを上書きしないようにするためです.
継承の実行
function SuperClass() { ... } ... function SubClass() { //親クラスのコンストラクタを呼ぶ this.temp = SuperClass; this.temp(); ... } inherit(SubClass, SuperClass);
注釈
this.temp によるメンバ関数呼び出し
JavaScriptには,super みたいなキーワードが使えませんので,一旦this.tempプロパティに親クラスのコンストラクタをセットしてから呼び出すという形をとっています.
これと似たような話ですが,一般にOOPでは,オーバーライドしているメソッドの中からオーバーライドする前のメソッドを呼ぶことがあります(Decoratorパターンとかそうですね).例えば,
function _SubClass_instanceMethod () { super.instanceMethod(); ... } SubClass.prototype.instanceMethod = _SubClass_instanceMethod;
などのように(注:上のコードは文法エラー).
これを扱うには,上記のように親クラスのメソッドを一旦何らかのプロパティに代入してから呼び出すのが便利です.次のようにします:
function _SubClass_instanceMethod () { this.temp = SuperClass.prototype.instanceMethod; this.temp(); ... } SubClass.instanceMethod = _SubClass_instanceMethod;
このように this.temp はテンポラリプロパティである,と決めてしまった方がいいでしょう.JavaScriptはもともとオブジェクト指向言語ではなくプロトタイプベース言語なので仕方ありません.
こうした場合,注意することがあります.つまり,次のようなコードは絶対に避けなければなりません:
function foo() { this.temp = bar; for (var i = 0; i < 10; ++i) { this.temp(); } }
なぜなら,bar 関数の中で this.temp 値が書き換えられている可能性があるからです.こうなると一回目のループでは正しく bar 関数が呼ばれたとしても,もし bar 関数内で this.temp の値が書き換えられていた場合 2 回目以降の this.temp() 呼び出しはおかしくなるでしょう.
この問題を避けるため,毎回 this.temp() 呼び出しの直前で this.temp に関数オブジェクトをセットする,というルールに従うようにしましょう.つまり上のコードは
function foo() { for (var i = 0; i < 10; ++i) { this.temp = bar; this.temp(); } }
とすればよいのです.本来ならtempの値を保持して元に戻すというコード:
function foo() { var save = this.temp; this.temp = bar; for (var i = 0; i < 10; ++i) { this.temp(); } this.temp = save; }
を this.temp を利用しているすべてのコードで行えばよいのですが,オーバーヘッドを考えればやはり簡潔な最初の方法をとることにしたほうがよいでしょう.
クラスメンバの継承
もしクラス変数とクラスメソッドも継承したいのであれば,継承関数 inherit を次のようにすればよいでしょう.
function inherit(subClass, superClass) { copy_undef_properties(superClass, subClass); copy_undef_properties(superClass.prototype, subClass.prototype); }
しかしここまでやる必要はないと思います.また,上のコードではオブジェクトの constructor と prototype を上書きするのではないかと心配する方もおられるかもしれません.しかしfor in 文では,この2つのプロパティは対象外となってますのでご安心を.
動的継承
動的継承?何それ? 動的継承みたいな高度なものなんか使わない,と思っている人がいるかもしれませんね.しかし,すでにHTMLエレメントとして存在しているオブジェクトを動的継承でカスタマイズするのは結構使えます.一度試してみてください.
動的継承関数 - dynamic_inherit
function dynamic_inherit(instance, dynamicClass, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { copy_undef_properties(dynamicClass.prototype, instance); //コンストラクタの呼び出し instance.temp = dynamicClass; instance.temp(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); return instance; }
dynamic_inherit関数の引数は,最大10個までのコンストラクタ関数の引数に対応できるように作られています.不細工ですが他にいい方法を思い付きません.
動的継承の実行
var obj = new Object(); dynamic_inherit(obj, dynamicClassA, argA1, argA2, ...); dynamic_inherit(obj, dynamicClassB, argB1, argB2, ...);
または
var obj = dynamic_inherit( dynamic_inherit(new Object(), dynamicClassA, argA1, argA2, ...), dynamicClassB, argB1, argB2, ...);
強制的な継承関数 - force_inherit, force_dynamic_inherit
例えば,ボタンなどHTMLエレメントの onclick プロパティに対して動的継承を使って動作を変えたい場合,上記の方法ではうまくいきません.IE4 では,すでにダミーの関数オブジェクトがセットされているためです.そこで,強制的に動的継承を行う関数を用意しましょう.それに加え,整合性のため強制的に継承を行う関数もつくっておきます.
function copy_properties(src, dest) { for (var prop in src) { dest[prop] = src[prop]; } } function force_inherit(subClass, superClass) { copy_properties(superClass.prototype, subClass.prototype); } function force_dynamic_inherit(instance, dynamicClass, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { copy_properties(dynamicClass.prototype, instance); instance.temp = dynamicClass; instance.temp(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); return instance; }
強制的動的継承を実行するのは以前のコードと同様です.
まとめ - JavaScriptクラスの標準形
以上を踏まえて,継承その他を考慮した場合のJavaScriptプログラミングスタイルは次のようにすればよいでしょう.
// コンストラクタとインスタンス変数 function Class() { this.temp = SuperClass; this.temp(); this.instanceVariable = initValue; } // 継承 inherit(Class, SuperClass); // インスタンスメソッド function _Class_instanceMethod () { .... } Class.prototype.instanceMethod = _Class_instanceMethod; // クラス変数 Class.classVariable = initValue; // クラスメソッド function _Class_classMethod() { ... } Class.classMethod = _Class_classMethod;
oop.js
ここまでに定義したオブジェクト指向プログラミング用の関数をまとめてoop.jsというファイルに入れることにしましょう.
function copy_undef_properties(src, dest) { for (var prop in src) { if (typeof(dest[prop]) == "undefined") { dest[prop] = src[prop]; } } } // 継承関数 function inherit(subClass, superClass) { copy_undef_properties(superClass.prototype, subClass.prototype); } // 動的継承関数 function dynamic_inherit(instance, dynamicClass, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { copy_undef_properties(dynamicClass.prototype, instance); instance.temp = dynamicClass; instance.temp(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); return instance; } function copy_properties(src, dest) { for (var prop in src) { dest[prop] = src[prop]; } } // 強制的な継承関数 function force_inherit(subClass, superClass) { copy_properties(superClass.prototype, subClass.prototype); } // 強制的な動的継承関数 function force_dynamic_inherit(instance, dynamicClass, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { copy_properties(dynamicClass.prototype, instance); instance.temp = dynamicClass; instance.temp(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); return instance; }
問題点
インクルードファイルの問題
オブジェクト指向の場合,再利用性を考慮してクラスごとにJavaScriptファイル(*.jsファイル)を作りたくなります.けれども,HTML文書内に簡単にインクルードする方法がありません.
例えば Superクラスの定義ファイル Super.js と Sub クラスの定義ファイル Sub.js ファイルを作って
<script type="text/javascript" src="Super.js"></script> <script type="text/javascript" src="Sub.js"></script>
とすればよいわけですが,クラス構成が複雑になることを考えて
<script type="text/javascript" src="Sub.js"></script>
だけですむようにしたいところです.
ところが,Sub.js ファイル内に
document.write("<script type='text/javascript' src='Super.js'" + "><" + "/script>");
のようなコードを入れても,IE4で試したところ, Sub.js が評価されたあと Super.js が評価されてしまいます.これでは,継承で親クラスが先に定義されなければならないのに,子クラスが先に定義されてしまってうまくいきません.
この問題は,オブジェクト指向の再利用から考えた場合に大きな障害となっています.クラス構成が複雑になればなるほどインクルードするファイルとその順序をユーザが考慮しなければならないのです.
なお,ASPなどが使える環境の場合,ASP側でクライアントのJavaScriptコードをすべて展開すればこの問題を回避できます.このとき,毎ページすべてのJavaScriptコードが展開されるので*.jsファイルをブラウザ側でキャッシュする,というようなことができなくなります.
おまけ
普段JavaScriptのコードはMeadowで書いています.Emacs-Lispでこの記事のプログラミングスタイル用メジャーモードを作成しました(javascript-mode.el).自由に使ってください.
このモードは,java-modeを拡張して次の3つのコマンドを定義しています.
コマンド名 | 機能 | キーバインド |
---|---|---|
javascript-class | カーソル位置にJavaScriptのコンストラクタを挿入する | \C-c c |
javascript-instance-method | カーソル位置にインスタンスメソッドを挿入する | \C-c i |
javascript-class-method | カーソル位置にクラスメソッドを挿入する | \C-c s |
javascript-mode.elを見ればわかると思いますが,僕のEmacs-Lispプログラミングははっきりいってタコです(T_T).何かアドバイスいただければ幸いです.というか,誰か作り直してください(笑).