JavaScript におけるクラスベースの継承方法色々
JavaScript Patterns: Build Better Applications with Coding and Design Patterns
- 作者: Stoyan Stefanov
- 出版社/メーカー: O'Reilly Media
- 発売日: 2010/10/01
- メディア: ペーパーバック
- 購入: 2人 クリック: 79回
- この商品を含むブログ (5件) を見る
先日、JavaScript におけるクラスベースの継承方法に関して @think49 さんと色々議論してたんですが、クラスベースの継承方法に関して 『JavaScript Patterns』 (和訳版 『JavaScript パターン』) の中にパターン化してまとめられていましたのでここで紹介しておきます。
JavaScript におけるクラスベースの継承って?
JavaScript にはクラスというものがありませんが、コンストラクタ関数を作り、new
演算子を使ってインスタンスを作るということが可能です。
var Constructor = function Constructor() {}; // コンストラクタ var c = new Constructor(); // インスタンス化
インスタンス化の記法が他のクラスベースの言語 (特に Java など) のものに近いため、コンストラクタ (prototype
プロパティなどを含む) を擬似的なクラスだとみなすことがあります。 本記事では、継承を用いてこの擬似的なクラスのコードを再利用する方法をクラスベースの継承という *1 ことにし、それについて紹介します。 簡単な紹介に留めますので、詳細は 『JavaScript Patterns』 の 6 章 (Code Reuse Patterns) をご覧ください。 以降、"Classical Pattern" という言葉が出てきますが、これは書籍中の言葉をそのまま使っています。
なお、書籍では 「再利用」 を目的としていますので必ずしも 「継承」 と言えないパターンもありますが、「再利用」 というのも締りが悪いので本記事では 「継承」 という言葉を使わせてもらいます。
前提
親となるコンストラクタ Parent
と、それを再利用する Child
があるとします。 特に断りが無い限りは、このコードを前提に話を進めていきます。
var Parent = function Parent( name ) { this.name = name || "Adam"; }; Parent.prototype.getGreetingMessage = function getGreetingMessage() { return "Hello, I'm " + this.name + "!"; }; var Child = function Child() {};
以降、5 つのパターンを紹介します。
Classical Pattern #1 - The Default Pattern
クラスベースの継承の最も一般的なパターンです (多分)。 単純に子となるコンストラクタの prototype
プロパティに、親のインスタンスを代入するというものです。
// Child のインスタンスが Parent のインスタンスをプロトタイプ継承する Child.prototype = new Parent(); // 必要に応じてメソッドの追加などを行う Child.prototype.newMethod = function newMethod() { /* ... */ };
単純なので JavaScript 関係の書籍でもしばしば目にすることがあり、一般的に使われている手法だといえます。 しかし、この方法ではクラスを継承しているのではなく、親クラスのあるインスタンスを継承していることになります。 上の例では、本来インスタンスごとに持っているべきだと思われる name
プロパティが、Child
インスタンスごとではなく Child
インスタンスの単一のプロトタイプオブジェクトにのみ存在しているという点で問題があります。 (もちろんそれが問題でないなら良いのですが。)
Classical Pattern #2 - Rent-a-Constructor
次に紹介するのは、子のコンストラクタ内から親のコンストラクタを呼び出す、というものです。
// 前提となる Child コンストラクタをこれに置き換える var Child = function Child( name ) { // 親のコンストラクタを呼び出す Parent.apply( this, arguments ); };
このパターンは、親のコンストラクタを再利用する、というものです。 親と子の間に直接的なプロトタイプ継承の関係はありません。
Classical Pattern #1 の欠点が解消され、親クラスで定義されているプロパティが、ちゃんと子のインスタンスごとに持たされるようになっています。 その一方で、親の prototype
プロパティで定義されているメソッドは継承されない、という欠点もあります。
Classical Pattern #3 - Rent and Set Prototype
上の 2 つのパターンをあわせたものがこのパターンです。
// 前提となる Child コンストラクタをこれに置き換える var Child = function Child( name ) { // 親のコンストラクタを呼び出す Parent.apply( this, arguments ); }; // prototype プロパティに親のインスタンスの設定する Child.prototype = new Parent(); // 必要に応じてメソッドの追加などを行う Child.prototype.newMethod = function newMethod() { /* ... */ };
これまでの 2 つのパターンの短所を解消できています。 ただ、親のコンストラクタ内で定義される name
プロパティが、子のインスタンスのプロトタイプオブジェクトにも存在し、また子のインスタンス自身のプロパティとしても存在する、(すなわち、name
プロパティが 2 回継承されている) というのが気になるところではあります。
Classical Pattern #4 - Share the Prototype
これは 「継承」 とは少し違いますが、プロトタイプオブジェクトを共有するというものです。
Child.prototype = Parent.prototype
このようなコードの再利用方法が有効な場面もあるかもしれませんが、基本的には使う必要のないパターンでしょう。
Classical Pattern #5 - A Temporary Constructor
最後は、#3 のパターンをさらに改良したものです。
// #3 のパターンにおける // Child.prototype = new Parent(); // の代わりに以下の処理を行う (function () { // ローカルスコープ生成 var F = function F() {}; // 一時的なコンストラクタ F.prototype = Parent.prototype; Child.prototype = new F(); })();
#3 のパターンでは Parent.prototype
を継承するために Child.prototype = new Parent()
としていましたが、この方法だと Parent
コンストラクタを実行してしまいます。 Parent
コンストラクタを実行せずに Parent.prototype
を継承するために、一時的なコンストラクタ F
を作って対処するというのがこのパターンです。
さらに改良
他のパターンではプロトタイプオブジェクトの constructor
プロパティについてはあまり考えてきませんでしたが、実際にはそういった点も考えたほうがいいでしょう。 ついでに親となるコンストラクタを子のコンストラクタのプロパティとして持たせておけばいいかもしれません。
(function () { // ローカルスコープ生成 var F = function F() {}; // 一時的なコンストラクタ F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; // constructor プロパティの設定 Child.baseConstructor = Parent; // 親コンストラクタを表すプロパティの設定 })();
まとめ
色々なパターンを紹介しましたが、クラスベースの継承という点では #5 の方法が最良だと思います。
ただし、JavaScript でクラスベースの継承を使う必要がある場面というのはそうそうないと思います。 他の言語に近いからという理由で安易にクラスベースっぽい書き方をするのではなく、本当に必要な場合にのみ使うようにしましょう。
参考文献
本記事の内容は、次の書籍の内容を紹介するものです。 詳細は書籍をご覧ください。
- JavaScript Patterns, Stoyan Stefanov 著
- 和訳版: JavaScript パターン, Stoyan Stefanov 著, 豊福 剛 訳
*1:書籍では "Classical Inheritance" と言っています。