JavaScript におけるクラスベースの継承方法色々

JavaScript Patterns: Build Better Applications with Coding and Design Patterns

JavaScript Patterns: Build Better Applications with Coding and Design Patterns

先日、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 でクラスベースの継承を使う必要がある場面というのはそうそうないと思います。 他の言語に近いからという理由で安易にクラスベースっぽい書き方をするのではなく、本当に必要な場合にのみ使うようにしましょう。

参考文献

本記事の内容は、次の書籍の内容を紹介するものです。 詳細は書籍をご覧ください。

*1:書籍では "Classical Inheritance" と言っています。