- 2007-12-17 (月)
- Javascript
Javascriptでオブジェクト指向なコードを書くには、prototypeベースな言語ゆえ、他のOO言語と異なり多少の小細工が必要になります。やり方は幾つもあるようですが、自分であれこれ試してみたうえでのまとめをここで共有してみます。
OOPと言っても、あくまで個人的に最低限必要だと思うこれら機能の実現を目的にしています:
- 子クラスのコンストラクタにて、親のコンストラクタを実行
- 他のOO言語では空気を吸うがごとく実装されている機能
- メソッドの継承
- Child.prototype = new Parent() なんて親インスタンスを作る事無くなんとかする
これらをいかに少ない手間で実現できるか。hacker諸氏ならばちょろっと頭使えばできるのでしょうが、自分はウダウダ時間かけて悩んでしまいました orz。以下3通りの解決策です。
目次
- 専用のextend関数を使う
- prototype.js v1.6.0 の Class.create() を抽出して利用
- ライブラリに頼らず、書き方を工夫して実現
- 終わりに
- 関連情報
- おまけ Download
1. 専用のextend関数を使う
Thousand Yearsさんの記事 JavaScript継承パターンまとめ にて紹介されていたエクステンドなる関数を使った手法が一番お手軽な模様:
function extend( s, c ){ function f(){}; f.prototype = s.prototype; c.prototype = new f(); c.prototype.__super__ = s.prototype; c.prototype.__super__.initialize = s; c.prototype.initialize = c; return c; };
使い方
// 親クラス Animal = extend( Object, function ( arg ){ this.name = arg.name || 'unknown'; this.voice = 'grunt'; }); Animal.prototype.bark = function (){ return this.voice; }; // 子クラス Dog = extend( Animal, function ( arg ){ this.__super__.initialize( arg ); this.voice = 'bow wow'; }); // 孫クラス Chiwawa = extend( Dog, function ( arg ){ this.__super__.initialize( arg ); this.voice = 'pow pow'; });
子/孫クラスのコンストラクタ内にて this.__super__.initialize() を実行することで、親のコンストラクタが呼び出される仕組み。Chiwawa の中では this.__super__ == Dog.prototype を、 Dog の中では this.__super__ == Animal.prototype をきちんと指し示す辺りが無名Function + prototype プロパティの妙技かと。Thousand Yearsさんに感謝。正直まだ把握しきれていません orz
サンプルおよびテストコード
ある程度きっちりと親・子・孫クラスの実装をした上で、一通りのテストコードを走らせてみたところ問題は無い模様。以下ソースです:
2. prototype.js v1.6.0 の Class.create() を抽出して利用
v1.5.x までの prototype.js で提供されていた Class.create() は特に何をしてくれるわけでは無かったのに対し、v1.6.0 ではググッと機能が充実して、今回の目的にぴったりなモノに仕上がっていました。こら使わない手は無い。
新 Class.create() の追加機能:
- 第一引数に親クラスを指定可能
- 任意のクラスメソッドにて、第一引数の変数名を $super にする事で親クラスの同メソッドを実行可能
使い方
簡単なコードで表すとこんな感じになります:
// 親クラス Animal = Class.create({ initialize: function ( arg ){ this.name = arg.name || 'unknown'; this.voice = "hi i'm animal"; }, bark: function { return this.voice; } }); // 子クラス Dog = Class.create( Animal, { initialize: function ( $super, arg ){ $super( arg ); this.voice = 'bow wow'; } });
Dogクラスを定義する際、Class.create() の第一引数に Animal を渡す事で親クラスを指定。これでメソッドの継承はOK。さらに、Dogクラスのコンストラクタ - initialize() - の第一引数名を $super にしておくと、$super に親クラスの同名メソッドが typeof $super == 'function' な形で渡ってくる。これを実行すると Animal.initialize.apply( this, arguments ) と同じように、this スコープは Dog なままに親の initialize を実行することができます。まあ便利。
サンプルおよびテストコード
親・子・孫クラスまで用意して、一通りのテストコードを書いてみたところすんなり通ったので、特にイレギュラーな問題は無い模様。以下ソースです:
Class.create() だけを抽出した単独ファイルを作ってみた
Class.create() の為に prototype.js ファイルをまるごと読み込むのは気が進まない...。v1.6.0 の prototype.js は通常サイズで122KB、改行等を除去したミニ版で90KBと、それなりのファイルサイズを毎回ブラウザにて読み込む事が必要になってきます。まぁユーザ的には大して気にならないのかもしれませんが、Class.create() 使いたいだけなのに、代償が100KB前後のファイル読み込みと言われると・・・開発者としてはちょっと気が進まないので、prototype.js のコードから Class.create() に必要な部分だけを抽出して単独ファイルを作ってみました。
Class.create() だけを抽出した単独ファイル:
もとの122KBに比べると5.4KBと劇的なサイズ減。これなら心的不安は一気に解消!かと。あるいは自身の js ファイルの冒頭辺りに上記ファイル中のコードをまるっとコピペして使うのもOKですね。イイと思って貰えるならば、どうぞご活用くださいませ。
※ついでに packer で圧縮したバージョンはこちら (3.0KB)
※抽出の副産物として、Class.create() の他にも Object.extend() と $A() が利用可能になっています。それと変数 $ には一切触れていません。
3. ライブラリに頼らず、書き方を工夫して実現
使い方
あるいは Class.create() とか、自作した専用の俺 class_extend() 関数とかには一切頼らず、クラス定義の書き方を工夫するだけで実現する方法はこちら:
// 親クラス Animal = function (){ this.init.apply( this, arguments ); }; Animal.prototype.init = function ( arg ){ this.name = arg.name || 'unknown'; this.voice = 'grunt'; }; Animal.prototype.bark = function (){ return this.voice; }; // 子クラス Dog = function (){ this.init.apply( this, arguments ); }; for ( var k in Animal.prototype ){ // メソッド継承 Dog.prototype[ k ] = Animal.prototype[ k ]; } Dog.prototype.init = function ( arg ){ Animal.prototype.init.apply( this, arguments ); this.voice = 'bow wow'; };
サンプルおよびテストコード
Class.create()の時と同様、親・子・孫クラスを用意したうえで同じようなテストコードを流しても問題は無い模様。以下ソースです:
ポイント1) メソッドはすべて *.prototype にて定義する
書こうと思えば *.prototype は使わずにコンストラクタやらメソッドやらをまとめて定義する事も可能ですが:
// NGパターン Animal = function ( arg ){ this.init = function ( arg ){ this.name = arg.name; this.voice = 'hello'; }; this.bark = function (){ return this.voice; }; this.init( arg ); // コンストラクタ実行 };
↑こう書いてしまうと、後で子クラス側でこれらメソッドを Animal.init() のように、インスタンス化せずに引っ張ってこれなくなってしまうのでNG。特にコンストラクタ init() は子クラスから呼び出す事が必須なので *.prototype オブジェクトの中で定義したうえで、apply() を使って実行すべし。
Animal = function ( arg ){ this.init.apply( this, arguments ); }; Animal.prototype.init = function ( arg ){ this.name = arg.name; this.voice = 'hello'; }; Animal.prototype.bark = function (){ return this.voice; };
apply()は本来関数の this スコープを一時的に変更させる為の機能(いわゆるdelegate)ですが、ここでは単に「引数をそのまま init() に渡しなおす」為に使っています。
コンストラクタが Animal.prototype.init の形で引っ張ってこれる状態であれば、子クラス Dog のコンストラクタにてこれを実行させることができるようになります。よっしゃ。
Dog.prototype.init = function ( arg ){ Animal.prototype.init.apply( this, arguments ); this.voice = 'bow wow'; };
this.super.init.apply() みたいにカッコよく呼べないのが難ですが。巧いやり方思いつかなかった orz
こっちの apply() こそ、this スコープを Dog として親の init() を実行するという本来の使い方ですね。
ポイント2) メソッド継承は *.prototype 同士のプロパティコピーで
よく見かける継承の記述:
Dog.prototype = new Animal();
はお手軽な反面、Animal のコンストラクタでやれることが限られてしまうのでNG。たとえば Animal コンストラクタの中で、引数で渡されたID値のエレメントの存在をチェックする... なんて実装:
Animal.prototype.init = function ( id ){ if( !document.getElementById( id ) ){ alert( '指定された要素は存在しません' ); } };
を書いてしまうと、前述の継承記述のタイミングで id == undefined な状態で init() が実行されて alert が出てしまいます。あるいは継承時の実行を示す何かを引数に渡すように書けば解決しますが:
Animal.prototype.init = function ( arg ){ if( arg.is_extends ){ return; // 継承時なら何もしない } // 以降、通常の処理 }; Dog.prototype = new Animal({ is_extends: true });
これもやや面倒くさい気が。なのでこれらを避ける為にも、継承の記述は:
for ( var k in Animal.prototype ){ Dog.prototype[ k ] = Animal.prototype[ k ]; }
と、for ループで親のメソッドやらプロパティやらをコピーしてあげるべし。
※継承関係を設定するのに Animal インスタンスを作るって、なんか気持ち悪るいかな、なんて。
※ただしこの方法はすべてのメソッド・プロパティを *.prototype として定義しておく前提のお話。
その他余談
この amachang with danさんな記事を読んでいて、以下の簡略的な書き方でもいけるか?と思ったけど
Dog.prototype = Animal.prototype;
これだとプロパティコピーでは無く、両方とも同じオブジェクトを指す形ゆえ、Dog.prototype.init = function (){ ... } と書き換えた時点で Animal.prototype.init も書き換わってしまう為、孫クラスの init() の中で Dog.prototype.init.apply() しようとすると deep recursion が起きちゃってNG。テストコードはこちら。む、残念。
4. 終わりに
以上、現時点で僕が調べた限りの情報をうだうだ書き連ねてみました。まだ javascript の事を良くわかっていないので、間違いやら勘違いやらを見つけた際には、こっそりコメント欄にて指摘していただけると幸いです m(_ _)m
5. 関連情報
- Defining classes and inheritance - prototype.js
- prototype.js 本家サイトの Class.create() チュートリアル記事(英語)
- Extend.js
- 似たような事を実現する為のライブラリ。いろいろ多機能。あるSEのつぶやきさんの記事経由。
- __proto__とprototypeについて - guccyonikkiさん
- 勉強させて貰いました。
- javascript packer
- javascriptファイルを圧縮
- JSAN - Test.Simple
- Javascriptでユニットテスト。今回のテストコードはこれを使っています。
6. おまけ Download
prototype.jsのミニ(改行除去)版やらpack版を本家サイトで見かけなかったので、別途作成したものをここでついでに共有しておきます。さらに記事では触れなかったjQueryも。ついでにww
- prototype.js 1.6.0 通常版 122K
- prototype.js 1.6.0 ミニ版 90K
- prototype.js 1.6.0 pack版 49K
- jQuery 1.2.1 通常版 79K
- jQuery 1.2.1 ミニ版 46K
- jQuery 1.2.1 pack版 27K
Comments:1
- cyokodog 2008-06-17 (火) 00:58
-
[1. 専用のextend関数を使う]で、this.__super__.initialize( arg );を使用すると以下のような問題が発生しませんか?
// 親クラス
Animal = extend( Object, function ( arg ){
this.name = arg.name || 'unknown';
this.voice = 'grunt';
});
Animal.prototype.bark = function (){
return this.voice;
};
// 子クラス
Dog = extend( Animal, function ( arg ){
this.__super__.initialize( arg );
this.voice = 'bow wow';
});
var dog = new Dog({name:'cyoko'})
alert(dog.name) //cyoko (OK)
var dog2 = new Dog({})
alert(dog2.name) //unknown (OK)
alert(dog.name) //unknown (NG) cyokoと表示されるべきでは?私も自己流OOPを考えていて同じような問題に遭遇したもので。。
ご参考までにhttp://d.hatena.ne.jp/cyokodog/20080616/1213632181