JavaScriptでの継承について調べてみた
JavaScriptはプロトタイプOO言語ということで、インスタンスコピーによる継承とカスタマイズを用いるのが素直な方法なのだと思う。JavaScriptでは、単純にインスタンスを丸々コピーする方法では効率が悪いということで、 prototype による非対称スコープチェーンを用いる方法と、prototypeへのプロパティのコピーを用いる方法がよく用いられている。
しかしながら、サイトによって良い悪いがばらばらで、結局これらの方法のどれを使うのが良いのかよく分からなかったので、どういう違いがあるのか調べてみた。
prototypeスコープチェーンを操作
参考文書:
- http://faces.bascule.co.jp/inheritance.html
- プログラマのためのJavaScript (11):継承についてもう少し - 檜山正幸のキマイラ飼育記 (はてなBlog)
- [鏡] C/C++ プログラマのための JavaScript 入門: prototype と継承 -- 戯れ言++
- http://developer.mozilla.org/ja/docs/Core_JavaScript_1.5_Guide:Property_Inheritance_Revisited
- 日本語版 O'REILLY JavaScript 第3版, p151-152
prototypeスコープチェーンを操作するのには以下の2つの方法がある。
prototype に スーパークラスのインスタンス投入
Subclass.prototype = new Superclass(); Subclass.prototype.constructor = Subclass; //必要があれば //以後、Subclassのprototypeをカスタマイズ
FlashやMozillaの内部など各地で使われていることから、ある程度一般的な手法として確立している。個人的にもこの方法が一番分かりやすいと思っているが、2行目のコンストラクタを元に戻すところがスマートでないと思う。constructorを使わなければ問題ないのかもしれない。
根本的な問題点は、クラスの定義時点でスーパークラスをnewしなければならないところ。プロトタイプOO言語だから仕方ないところではあるのだけども、クラス定義時のスーパークラスのコンストラクターの動作に気をつける必要がある。具体的には、クラス定義時とサブクラスのインスタンス生成時のコンストラクターの動作を分ける(つまり、スーパークラスは継承されることを意識する必要がある)とか、スーパークラスのnewのオーバーヘッドが大きくならないようにするとか。
スーパークラスは定義時にnewされているので、インスタンス生成時にスーパークラスのプロパティを改めて初期化する必要がある場合は、それ用の初期化の仕組みを用意しなければならない。
__proto__ に prototype 投入
Subclass.prototype.__proto__ = Superclass.prototype; //以後、Subclassのprototypeをカスタマイズ //必要があればSubclassのコンストラクタ内で Superclass.apply(this,args);
1行で済むためスマートであり、JavaScriptの本にも書いてある方法。newが出てこないため、静的な定義が可能になり、スーパークラスは継承されることを知らなくても大丈夫そう。
ただし、IEやJScriptのように__proto__を使えない実装があるため一般的でない。
prototype.__proto__ という言語実装が表に出てきたような構文になってしまうため、無理やり感が漂ってかっこよくない気がしている。
オブジェクトプロパティコピー
スコープチェーンのつながりを作らずに、スーパークラスのプロパティをサブクラスのprototypeにコピーすることで継承を実現。
素直にコピー
//必要があればSubclassのコンストラクタ内で Superclass.apply(this,args); //Subclassのコンストラクタ定義後に inherit(Subclass,Superclass);//ここでプロパティをコピー
似たような実装はいろいろなところにあって、 extend という名前のときもある。
この方法も不必要なスーパークラスのnewが要らないということで、静的な定義が可能になる。
必要なコードも少なく分かりやすい。また、インスタンスにプロパティーをコピーしてしまうことも出来るため、インスタンスの挙動を動的に変更させることが出来る。
欠点というか上のスコープチェーンと違うところは、inherit以降にスーパークラスのプロパティを変更した場合はサブクラスに反映されないところ。
「古典的クラス指向」
基本的に戦略は上と同じ。prototypeを表に出さずにメソッドを定義したり継承をしようというところを「古典的」といっているよう。これも継承はプロパティコピーで実現されている。
新しい世界を構築
Superclass = Class.create(); Superclass.prototype = { initialize: function(args) { //コンストラクタ }, methods: function(args) { },... }; Subclass = Class.create(); Object.extend(Subclass.prototype,Superclass.prototype);
オブジェクトの性質を決めるプロパティは上と同じプロパティのコピーを行うが、スコープチェーンは使わない。その代わり、コンストラクタの仕組みを独自に用意することで、「古典的クラス指向」人にとって直感的なクラス構築を行う。extendによるプロパティコピーはRubyのincludeに見えてくる。
ただし、スーパークラスのコンストラクタはextendの際に上書きしてしまうので、本格的に使う場合は prototype.js のソースをちゃんと読んで理解してから使った方が良いと思われる。
JavaScriptの継承に期待するもの
結局、どれを使用するべきかはJavaScriptのOOPに何を期待しているかなのだと思う。
OOPの一般的機能について
代表的なOOPの機能に期待するものといえば、以下の点が挙げられる。
上3つはどの方法でも普通に可能であると思っている。
問題は4番目のクラス階層で、 instanceof でクラス階層を調べたり、インスタンスの型をチェックする場合に、prototypeチェーンの仕組みが必要になってくる。
プロパティコピーでも独自機構でクラス階層を実現できる。しかし、外部とのインタフェースが少ない場合はJavaScriptで厳密な型チェックをやることにあまり意味は無いと思われるので、プロパティコピーでさっと機能をコピーして使うというのがJavaScript的といえるのかもしれない。
継承の動作について
次は、細かな動作の違いについて考えてみる。
コンストラクタによる初期化という視点から見ると、プロパティコピーはメソッドとプロパティー値の静的コピーということになるのでスーパークラスのコンストラクタの呼び出しは自分で制御しなければならない。__proto__を使う方法も同様。必要があればサブクラスでスーパークラスのコンストラクタを呼べば済むように思う。
一方、prototypeにnewする方法はクラス定義の時点でスーパークラスのコンストラクタが実行されてしまう。そのため、スーパークラスのコンストラクタでは継承時にも実行されて構わないような内容(複雑な初期化をしないとか、インスタンス生成時と継承時を分けるとか)になっている必要がある。
基本クラスを動的に変更して、サブクラスやインスタンスの挙動を一気に変更したいというような場合についてはprototypeチェーンの仕組みを使うと楽なはず。基本クラス変更の手法の是非については議論が分かれるが、サブクラスの性質がプロパティコピーのタイミングで固定されてしまうというのも積極的な欠点になりそうだと思う。
動的なメタプログラミングに着目した場合、スコープチェーンとプロパティコピーの違いは大きくなりそうであるが、個人的にJavaScriptでのメタプログラミング手法に慣れていないためよく分からない。
結局どの継承を選ぶべきか?
まず、スコープチェーンが必要かどうかがポイントになると思う。クラス階層のチェックや、スーパークラスの動的機能追加などが含まれる場合は、スコープチェーンの仕組みがあったほうが便利になる。
次に、スーパークラスをnewするのが気になるかどうか、コンストラクタに求められる機能とか、継承されることを意識してもよいかどうか、__proto__が使えるかどうかを考慮して、どの方式で行くのかを最終的に決定するということになるのだと思う。
そうすると、とりあえず使いやすいプロパティコピーで十分かどうかを検討して、スコープチェーンが必要な場合に限って、 prototype new とか __proto__ とか、独自機構などを考えるといいではないかという結論に至った。