SSブログ

JavaScrip のクラスの多重継承(の、ようなもの) [JavaScript]

◆JavaScrip のクラスの多重継承(の、ようなもの)

俺さあ、C++ あがりだからさあ、「クラス」と聞いたら、どんな言語でも「多重継承できたらな~」って思うんですよ。

ということで、JavaScript でも試してみましょう。

「多重継承マニア」の方は、お楽しみくださいw。


まず、スーパークラスのひとつとして、「クラスの作成」エントリー でも登場した、ClassA を使います。

function ClassA() {
    this.alertWord = function (word) { alert(word); }
    this.alert1 = function () { this.alertWord('ClassA 1'); }
    this.alert3 = function () { this.alertWord('ClassA 3'); }
    return this;
}
ClassA.prototype.constructor = ClassA;
ClassA.prototype.alert2 = function () { this.alertWord('ClassA 2'); }

さらに、前々々々回のエントリーで作った、以下の関数。

// 任意のオブジェクトのすべての属性を列挙
function enumAllProperties(refObject) {
    // 引数:refObject ... 属性を列挙したいクラス
    // 戻値:String ...... 属性の列挙結果文字列(ひとつ1行で改行をはさむ)
    var strProperties = "";
    for (var item in refObject) {
        strProperties = (strProperties + item + " = " + refObject[item] + "\n");
    }
    return strProperties;
}

// クラスのすべての prototype の列挙
function enumAllPrototypes(classType) {
    // 引数:classType ... プロトタイプを列挙したいクラス
    // 戻値:String ...... プロトタイプの列挙結果文字列(ひとつ1行で改行をはさむ)

    return enumAllProperties(classType.prototype);
}

enumAllProperties 関数は指定したオブジェクトの属性のすべて(プロトタイプ含む)を、enumAllPrototypes 関数は引数に与えたクラスが持つプロトタイプを列挙して、文字列化して返してくる関数です。

それから、前回のエントリーで作った、以下の関数。

// クラスのひとつの prototype の継承
function inheritPrototype(classSub, classSuper, item) {
    // 引数:classSub ..... サブクラス(派生クラス)
    //        classSuper ... スーパークラス(基本クラス)
    //        item ......... 継承したい prototype 属性
    classSub.prototype[item] = classSuper.prototype[item];
}

// クラスのすべての prototype の継承

function inheritAllPrototypes(classSub, classSuper) {
    // 引数:classSub ..... サブクラス(派生クラス)
    //        classSuper ... スーパークラス(基本クラス)
    for (var item in classSuper.prototype) {
        inheritPrototype(classSub, classSuper, item)
    }
}

この inheritAllPrototypes 関数に、サブクラスとスーパークラスを喰わせると、その時点でのスーパークラスのプロトタイプ プロパティの「コピー」を、サブクラスに与えることができます。

ただし、この関数は、前回のエントリーで示した通り、スーパークラスのプロトタイプ プロパティの動的な変化が、サブクラスに反映されない点にご注意ください。

 こいつらにも、今回頑張ってもらいましょう。

次に、もうひとつのスーパークラスとして、ClassA とよく似た構成の ClassB を準備します。

function ClassB() {
    this.alert2 = function () { alert('ClassB 2'); }
    this.alert3 = function () { alert('ClassB 3'); }
    return this;
}
ClassB.prototype.constructor = ClassB;
ClassB.prototype.alert1 = function () { alert('ClassB 1'); }

両方のスーパークラスでメソッドが競合しますが、これは「わざと」です。

この ClassAClassB を、ひとつのサブクラスで継承します。


最初に、callapply だけで実装する例。

function ClassE() {
    ClassA.call(this);  // あるいは apply
    ClassB.call(this);  // あるいは apply

    this.test = function () {
        this.alert1();
        this.alert2();
        this.alert3();
    }
    return this;
}
ClassE.prototype.constructor = ClassE;

この call または apply のみを使用する例では、「クラスの継承」のエントリーで示したように、各々のスーパークラスのプロトタイプ プロパティは、サブクラスに継承されません

call や apply で継承できるのは、スーパークラスのインスタンス プロパティだけで、サブクラスにインスタンス プロパティとして引き継がれます。

インスタンス プロパティは「値」であって、スーパークラスの内容を動的に変化させても、サブクラスには反映されませんから、それを念頭に置いたスーパークラスの設計が必要です。

この例でサブクラスに継承されるのは、ClassA.alertWord、ClassA.alert1、ClassA.alert3、ClassB.alert2、ClassB.alert3 で、継承の中で双方のスーパークラスの競合が発生するのは、alert3 メソッドだけです。

これで、var ins = new ClassE(); ins.test(); を実行すると、alert が "ClassA 1" → "ClassB 2" → "ClassB 3" の順番でポップアップします。

ふたつのスーパークラスで alert3 メソッドが競合するんですが、ClassB のメソッドの方が生きています。

ちなみに、call する順番を変えて・・・・・・、

function ClassE() {
    ClassB.call(this);  // あるいは apply
    ClassA.call(this);  // あるいは apply

    this.test = function () {
        this.alert1();
        this.alert2();
        this.alert3();
    }
    return this;
}
ClassE.prototype.constructor = ClassE;

これで、var ins = new ClassE(); ins.test(); を実行すると、alert が "ClassA 1" → "ClassB 2" → "ClassA 3" の順番でポップアップします。

今度は、ClassA の alert3 メソッドが生きています。

つまり、ふたつのスーパークラスのメンバに競合があってもエラーとはみなされず、後の方に call(または apply)されたものの方が、サブクラスの中で勝つわけです。

何度も書いてきた「後勝ち」ですね。


一方で、ひとつだけだけどプロトタイプは生きているわけだから、複数のクラスを継承するにしても、どれかひとつのスーパークラスのインスタンスをサブクラスのプロトタイプに乗せて、他のスーパークラスはサブクラスのコンストラクタから call apply する・・・・・・というのも、手段のひとつです。

その例が、これ。

function ClassF() {
    ClassA.call(this);  // ClassA インスタンス継承
    ClassB.call(this);  // ClassB インスタンス継承
    this.test = function () {
        this.alert1();
        this.alert2();
        this.alert3();
    }
    return this;
}
ClassF.prototype = new ClassA();  // ClassA プロトタイプ継承
ClassF.prototype.constructor = ClassF;

prototype にスーパークラスのインスタンスをぶち込むと、スーパークラスのプロパティすべてが(インスタンス、プロトタイプのどちらも)サブクラスのプロトタイプ プロパティとして継承されます。

プロトタイプ プロパティは「参照」であって、スーパークラスのプロトタイプ プロパティの内容を動的に変化させると、サブクラスにも反映されるので、ここで上げた例の ClassA 側のスーパークラスの設計は、自由度が向上しますね。

また、ClassA call も実行しているので、ClassA のインスタンス プロパティも、ClassF のインスタンス プロパティとして継承され、ClassF 内で発生する ClassA(または ClassF)内の同名のインスタンス プロパティとプロトタイプ プロパティの競合は、「インスタンス プロパティが優先的に呼び出される」というルールで、バッティングの回避が可能です。

この状態だと、ふたつのスーパークラスからの継承で、alert2 と alert3 が競合を起こします。

これで、var ins = new ClassF(); ins.test(); を実行すると、alert が "ClassA 1" → "ClassB 2""ClassB 3" の順番でポップアップします。

こちらでも、ふたつのスーパークラスのメンバに競合があってもエラーとはみなされず、prototype でインスタンスを作られたものより、call(または apply)されたものの方が、サブクラスの中で勝ち残ります。

ClassF の例の中では、alert3 だけではなく、ClassA の alert2 もプロトタイプに乗ったはずなので、ClassB.call によって、ClassB のメンバが勝ったわけですね。

この理由は単純で、callapply で継承されたプロパティは、プロトタイプ プロパティではなく、インスタンス プロパティだからです。

同名の複数のプロパティの名前解決に関して、プロトタイプよりインスタンスが優先されるのは、「クラスの作成 - 1」のエントリーで書いた通りです。


ところが、前述の2つの方法では、複数のスーパークラスのプロトタイプ プロパティを全部多重継承することができません。

とはいえ、prototype にスーパークラスのインスタンスをぶち込む方法では、継承できるプロトタイプ プロパティは、ひとつのスーパークラスに限定されます。

複数のスーパークラスを継承するには、やっぱり callapply を頼るしかありません。

でも、callapply は、プロトタイプ プロパティの面倒を見てくれません。

callapply がプロトタイプの面倒を見てくれないなら、自前でプロトタイプを処理しちゃいましょう

こんな感じで。 

function ClassG() {
    ClassA.call(this);  // ClassA インスタンス継承
    ClassB.call(this);  // ClassB インスタンス継承
    this.test = function () {
        this.alert1();
        this.alert2();
        this.alert3();
    }
    return this;
}
ClassG.prototype = new ClassA;  //  ClassA プロトタイプ継承
ClassG.prototype.constructor = ClassG
inheritAllProperties(ClassG, ClassB);  // ClassB プロトタイプのコピー

この場合でも、「後勝ち」の法則は生きているので、継承の順番には気をつけてください。

上記の順番では、インスタンス プロパティでもプロトタイプ プロパティでも、ClassB のプロパティ定義が「後勝ち」します。

ClassAインスタンス プロパティ「後勝ち」させたい場合は call または apply  メソッド呼び出し順を逆にしてください。

また、inheritAllProperties 関数で ClassB のプロトタイプ プロパティに「後勝ち」をさせたくない場合は、inheritAllProperties 関数内で、その時点ですでに ClassG に定義されているプロトタイプ プロパティの再定義を避ける・・・・・・というテクニックが必要になりそうです。

ただし!

何回も書きますが、inheritAllProperties 関数は、その時点でのスーパークラスのプロトタイプ プロパティの「コピー」をサブクラスに与えるだけなので、スーパークラスのプロトタイプ プロパティの動的な変化は、サブクラスに反映されません

そういう意味では、inheritAllProperties 関数を使ったプロトタイプ プロパティのコピーは、「なんちゃって継承」にすぎません。

というわけで、この方法は「多重継承(の、ようなもの)になるわけです。


JavaScript の「多重継承(の、ようなもの)の最終的な形は、核となるスーパークラスをひとつ決めて、そのスーパークラスの継承には、スーパークラスのインスタンスを作成して prototype にぶち込む方法と call apply で継承する方法の併用がいいのかな、と。

かつ、核としなかった他のスーパークラスの継承は、callapply を使う方法と、inheritAllProperties 関数のような、その時点でのスーパークラスのプロトタイプ プロパティの「コピー」を併用する方法になると思います。

この場合、スーパークラスの動的なプロパティの変化がサブクラスに反映されるのは、核となるスーパークラスのプロトタイプ プロパティだけで、それ以外は継承処理を実行した時点での、(言わば)静的な継承にならざるを得ないので、注意が必要です。

複数のスーパークラスのメンバ間で競合が起きても、実行時エラーとはならずに、サブクラスの中でどれかが勝ち残るというのは、クラス設計をしっかりやらないと、バグの温床になりそうな感じです;;;。

しかし、「多重継承(の、ようなもの)が実装できるだけでも、「多重継承マニア」の俺にとっては、生唾ものですw。

スリリングでアクロバチックなクラス設計ができそうですw。


nice!(2)  コメント(0)  トラックバック(0) 
共通テーマ:パソコン・インターネット

nice! 2

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

JavaScrip のクラスの継承getElementById ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。