ECMAScriptの継承メカニズムの実装

継承メカニズムの実現

ECMAScriptで継承メカニズムを実現するには、継承したい基底クラスから始めることができます。すべての開発者が定義したクラスは基底クラスとして使用できます。セキュリティの理由から、ローカルクラスやホストクラスは基底クラスとして使用できません。これにより、公共にアクセスできるブラウザレベルのコンパイル済みコードを防ぐことができます。これらのコードは悪意のある攻撃に使用されることがあります。

基底クラスを選定した後、そのサブクラスを作成することができます。基底クラスの使用は完全にあなたの決定に任せられます。時には、直接使用できない基底クラスを作成したい場合があります。それは、サブクラスに一般的な関数を提供するためだけに使用される場合があります。この場合、基底クラスは抽象クラスとして見なされます。

ECMAScriptは他の言語のように抽象クラスを厳しく定義していないものの、時には使用できないクラスを作成することもあります。通常、このようなクラスを抽象クラスと呼びます。

作成されたサブクラスは、超クラスのすべての属性とメソッドを継承します。これには、コンストラクタやメソッドの実装も含まれます。覚えておいてください、すべての属性とメソッドは共有されていますので、サブクラスはこれらのメソッドを直接アクセスできます。サブクラスは、超クラスにない新しい属性やメソッドを追加することもできますし、超クラスの属性やメソッドをオーバーライドすることもできます。

継承の方法

他の機能と同様に、ECMAScript が継承を実現する方法は複数あります。これは、JavaScript の継承メカニズムが明確に規定されていないため、模倣によって実現されているからです。これは、すべての継承の詳細が完全に解釈プログラムによって処理されるわけではありません。開発者として、最も適切な継承方法を選択する権利があります。

以下に具体的な継承方法をいくつか紹介します。

オブジェクト仮装

ECMAScript の原案を考えると、オブジェクト仮装(object masquerading)を設計することは全く考えていませんでした。それは、開発者が関数の動作方法や特に this キーワードの使用方法を理解し始めた後で発展しました。

その原理は以下の通りです:構造関数は this キーワードを使用してすべての属性とメソッドに値を割り当てます(つまり、クラス宣言の構造関数の方法です)。構造関数は単なる関数であるため、ClassA の構造関数を ClassB のメソッドとして使用し、それを呼び出すことができます。ClassB は ClassA の構造関数で定義された属性とメソッドを受け取ります。例えば、以下のように ClassA と ClassB を定義します:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}
function ClassB(sColor) {
}

覚えていますか?キーワード this は、コンストラクタが現在作成しているオブジェクトを参照します。ただし、このメソッドでは、this が指すオブジェクトの所有者です。この原理は、ClassA を構造関数としてではなく、通常の関数として継承メカニズムを構築する原理です。以下のように使用する構造関数 ClassB で継承メカニズムを実現できます:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

このコードでは、ClassA に newMethod メソッドを割り当てています(関数名はそのポインタを指すだけです)。そのメソッドを呼び出し、ClassB のコンストラクタの引数 sColor を渡します。最後の行のコードで ClassA の参照を削除することで、以降その呼び出しはできなくなります。

すべての新しい属性とメソッドは、新しいメソッドの行を削除した後に定義する必要があります。さもなければ、スーパークラスの関連する属性とメソッドをオーバーライドする可能性があります:

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

前のコードが有効であることを証明するために、以下の例を実行できます:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//出力 "blue"
objB.sayColor();	//出力 "red"
objB.sayName();		//「John」を出力します

オブジェクト仮装は多重継承を実現できます

面白いことに、オブジェクト仮装は多重継承をサポートしています。つまり、一つのクラスは複数のスーパークラスを継承することができます。UML で表現される多重継承メカニズムは以下の図の通りです:

継承メカニズム UML 図示例

例えば、ClassX 和 ClassY の二つのクラスが存在する場合、ClassZ がこれらのクラスを継承したい場合は、以下のコードを使用することができます:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

TIY

ここに一つの欠点があります。ClassXとClassYという二つのクラスが同名の属性やメソッドを持っている場合、ClassYが高優先権を持っています。なぜなら、それは後のクラスから継承されているからです。この小さな問題を除いて、オブジェクトの偽装を通じて多重継承メカニズムを実現することは非常に簡単です。

このような継承方法の普及により、ECMAScriptの第3版ではFunctionオブジェクトに二つのメソッドが追加されました。これらはcall()とapply()です。

call() メソッド

call() メソッドは古典的なオブジェクトの偽装方法と最も似た方法です。最初のパラメータはthisのオブジェクトとして用いられます。他のパラメータは関数自身に直接渡されます。例えば:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

この例では、sayColor()関数がオブジェクトの外で定義されており、それがどのオブジェクトにも属していない場合でも、キーワードthisを参照できます。オブジェクトobjのcolor属性はblueです。call()メソッドを呼び出す際、最初のパラメータはobjであり、sayColor()関数の中のthisキーワードにobjの値を割り当てることを示しています。二つ目と三つ目のパラメータは文字列であり、sayColor()関数の中のパラメータsPrefixとsSuffixと一致します。最終的に生成されるメッセージ「The color is blue, a very nice color indeed.」が表示されます。

このメソッドを継承メカニズムのオブジェクトの偽装方法と一緒に使用するには、前三行の代入、呼び出し、削除のコードを置き換えるだけで良いです:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

ここでは、ClassAの中のキーワードthisが新しいClassBオブジェクトに等しくなるようにする必要があります。したがって、thisは最初のパラメータです。二つ目のパラメータsColorは二つのクラスともにユニークなパラメータです。

apply() メソッド

apply() メソッドには二つのパラメータがあり、thisのオブジェクトと関数に渡すパラメータの配列として用いられます。例えば:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

この例は前の例と同じですが、現在は apply() メソッドを呼び出しています。apply() メソッドを呼び出す際には、最初の引数は引き続き obj であり、sayColor() ファンクションの this キーワードに obj を割り当てることを意味します。第二引数は、sayColor() ファンクションの引数 sPrefix と sSuffix に一致する二つの文字列からなる配列であり、生成されるメッセージは「The color is blue, a very nice color indeed.」であり、表示されます。

このメソッドは、前の三行の代入、呼び出し、新しいメソッドの削除コードを置き換えるためにも使用されます:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

同様に、最初の引数は引き続き this であり、次の引数は color という値を持つ配列です。ClassB の arguments オブジェクト全体を apply() メソッドの第二引数として渡すことができます:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

もちろん、超クラスの引数の順序が子クラスの引数の順序と完全に一致する場合のみ、引数オブジェクトを渡すことができます。そうでない場合は、正しい順序でパラメータを配置するために独立した配列を作成する必要があります。さらに、call() メソッドを使用することもできます。

プロトタイプチェーン(prototype chaining)

この形の継承は、ECMAScript では元々プロトタイプチェーンに用いられていました。前章でクラスのプロトタイプ方式の定義について紹介しました。プロトタイプチェーンはこの方法を拡張し、興味深い継承メカニズムを実現します。

前章で学んだように、prototype オブジェクトはテンプレートであり、インスタンス化されるオブジェクトはすべてこのテンプレートに基づいています。要約すると、prototype オブジェクトのすべての属性とメソッドはそのクラスのすべてのインスタンスに引き継がれます。プロトタイプチェーンはこの機能を利用して、継承メカニズムを実現します。

もし前の例のクラスをプロトタイプ方式で再定義する場合、以下の形式になります:

function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();

プロトタイプ方式の驚くべき点は、ハイライトされた青いコード行です。ここでは、ClassBのプロトタイプ属性をClassAのインスタンスに設定します。これは非常に面白いです。なぜなら、ClassAのすべての属性とメソッドを取得したいが、それらをClassBのプロトタイプ属性に個別に追加することを望まないからです。ClassAのインスタンスをプロトタイプ属性に割り当てる他に良い方法はありますか?

注意:ClassAの構造関数を呼び出し、それに引数を渡しません。これはプロトタイプチェーンにおける標準的な方法です。構造関数に引数が無いことを確認してください。

オブジェクト偽装と同様に、子クラスのすべての属性とメソッドは、プロトタイプ属性に値が割り当てられる後に表示される必要があります。なぜでしょうか?それは、プロトタイプ属性が新しいオブジェクトに置き換えられるため、元のオブジェクトに追加されたメソッドは削除されるからです。したがって、ClassBクラスにname属性とsayName()メソッドを追加するコードは以下の通りです:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
};

以下の例を実行することで、このコードのテストができます:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

TIY

さらに、プロトタイプチェーンでは、instanceof演算子の動作も非常にユニークです。ClassBのすべてのインスタンスでは、instanceofはClassAとClassBの両方に対してtrueを返します。例えば:

var objB = new ClassB();
alert(objB instanceof ClassA);	//「true」を出力します
alert(objB instanceof ClassB);	//「true」を出力します

ECMAScriptの弱いタイプの世界では、これは非常に役立つツールですが、オブジェクト偽装を使用する際にはそれを使用できません。

プロトタイプチェーンの欠点は、多重継承をサポートしないことです。覚えておいてください、プロトタイプチェーンは、クラスのプロトタイプ属性を書き換えるために別のタイプのオブジェクトを使用します。

ミックスイン方式

この継承方法は、クラスを定義する際に構造関数を使用し、原型を使用しない方法です。オブジェクト偽装の主な問題は、構造関数方式を使用する必要があることです。これは最適な選択ではありません。しかし、プロトタイプチェーンを使用すると、引数付きの構造関数を使用することができません。開発者はどのように選択すればよいのでしょうか?答えはシンプルです。どちらも使用します。

前章では、クラスを作成する最善の方法として、属性はコンストラクタで定義し、メソッドはプロトタイプで定義することを説明しました。この方法は、インヘリットメカニズムにも適用できます。コンストラクタの属性をオブジェクトでインヘリットし、プロトタイプオブジェクトのメソッドをプロトタイプチェーンでインヘリットします。これらの方法を使用して前の例を書き換え、以下のようになります:

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
};

在此例子中,继承メカニズムは、2行目でハイライトされた青いコードで実現されています。第1行目でハイライトされたコードでは、ClassBのコンストラクタ内で、ClassAのsColor属性をオブジェクトでインヘリットしています。第2行目でハイライトされたコードでは、プロトタイプチェーンを通じてClassAのメソッドをインヘリットしています。このようなハイブリッドメカニズムでは、プロトタイプチェーンが使用されているため、instanceof演算子も正しく動作します。

下面的例子测试了这段代码:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//出力 "blue"
objB.sayColor();	//出力 "red"
objB.sayName();	//出力 "John"

TIY