SSブログ

JavaScrip のクラスの継承 [JavaScript]

◆JavaScript のクラスの継承

「クラス」が作れるんならさあ、新しいクラスを作るときに、もう使える道具を持ってるクラスから派生(継承)して、楽したいよね。

ってことで、今回は「クラスの継承」です。

以前のエントリーで、クラス ClassA を作成しました。

ここから、新しいクラスを作成します。

【注意】 2010/12/23/ 22:50 追記があります。


まずは、前回前々回前々々回のエントリーの「クラス定義」を、改めて提示しておきます。

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'); }

今回のエントリーでも、これをいろいろと「こねくりまわし」ます。

それから、基礎知識。

普通の高級言語では、継承元のクラスのことを「基本クラス(または基底クラス)」、継承するクラスのことを「派生クラス」と言います。

しかし、JavaScript では、継承元のクラスのことを「スーパークラス」、継承するクラスのことを「サブクラス」と言うのが主流のようです。

もし、俺がエントリーの中で言葉を間違えたら、お手数ですが脳内変換をお願いします;;;。 

あと、前々々回のエントリーで作った、以下の関数。

// 任意のオブジェクトのすべての属性を列挙
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 関数は引数に与えたクラスが持つプロトタイプを列挙して、文字列化して返してくる関数です。

こいつらが、今回も役に立ってくれます。


最初は、オーソドックスな方法から行きましょう。

プロトタイプを使う方法です。

今度は、区別するために、ClassC という名前にしておきましょう。

その例は、こちら。

// クラス(コンストラクタ)定義
function ClassC() {
    this.test = function () {
        this.alert1();
        this.alert2(); // ←使える
        this.alert3();
    }
    return this;
}
ClassC.prototype = new ClassA();

派生クラスの prototype に基本クラスのインスタンスを作成してぶち込む方法です。

こうすると、ClassA で定義されているインスタンス プロパティプロトタイプ プロパティのすべてが、ClassC のプロトタイプ プロパティに引き継がれます(継承)

つまり、ClassC のインスタンスを作成すると、以下のメソッドが使えるんです。 

var ins = new ClassC();
ins.alertWord(word);  // ClassA インスタンスから継承
ins.alert1();               // ClassA インスタンスから継承
ins.alert2();               // ClassA プロトタイプから継承
ins.alert3();               // ClassA インスタンスから継承
ins.test();                 // ClassC でインスタンス プロパティ実装

なぜか?

答 えは簡単、この基本クラスである ClassA のインスタンスが、既に作成された状態で ClassC の prototype に乗っているから、ClassA で動的に追加される プロトタイプ プロパティ alert2 メソッドを含めて、ClassA がまるごと ClassC のプロトタイプに乗っかっているんですね。

ご興味とお時間のある方は、先程の enumAllPrototypes 関数に ClassC を喰わせて、結果をご確認ください。

この方法の特徴として、インスタンスが2つできちゃうことが挙げられます。

ClassC のプロトタイプの設定として、一度 ClassA のインスタンスを作ります。

それとは別に、ClassC のインスタンスが作られるんです。

もうひとつ。

スーパークラスのプロパティは、インスタンス プロパティだろうが、プロトタイプ プロパティだろうが、サブクラスではプロトタイプ プロパティとして継承されます。

ここは注意が必要ですね。

それから。

プロトタイプ プロパティは「参照」なので、スーパークラスのプロトタイプ プロパティを動的に変化させると、サブクラスのプロトタイプ プロパティも動的に変化します!

ダイナミック~!

ここで、ですね・・・・・・・。

ブラウザ依存で、なにが起きるかわからない JavaScrip。

一応、新しくサブクラスを作った場合は、コンストラクタの付け替えをしておいた方が、確実だと思います。

// クラス(コンストラクタ)定義
function ClassC() {
    this.test = function () {
        this.alert1();
        this.alert2(); // ←使える
        this.alert3();
    }
    return this;
}
ClassC
.prototype = new ClassA();
ClassC
.prototype.constructor = ClassC;
// コンストラクタの付け替え

こんな風に。


さて、クラスの継承のもうひとつの方法です。

スーパークラスの call メソッドや apply メソッドを使う方法です。

// サブクラス定義
function ClassD() {
    ClassA.call(this);    // クラスの継承
    this.test = function () {
        this.alert1();
        // this.alert2(); // ←使えない
        this.alert3();
    }
    return this;
}
ClassD.prototype.constructor = ClassD;

ここでは、ClassD のコンストラクタの中で、ClassAcall メソッドを呼び出して、クラスの継承をしています。

callapply メソッドを使うには、第一引数にサブクラス自身のインスタンス参照を示す this、第二引数以降にスーパークラスのコンストラクタが必要とする引数を列挙します。

スーパークラスのコンストラクタを呼び出すとき、引数の数が決まっていれば call、可変ならば apply を使います。

ここでは、スーパークラス ClassA のコンストラクタにに引数がないため、call を使い、第二引数以降は書きません。

もし、ClassA のコンストラクタが可変数の引数を取る場合、こういう書き方をします。

function ClassD() {
    ClassA.apply(this, arguments);    // クラスの継承
    this.test = function () {
        this.alert1();
        // this.alert2(); // ←使えない
        this.alert3();
    }
    return this;
}
ClassD.prototype.constructor = ClassD;

これで、クラス ClassD のインスタンスを作成すると、以下のメンバを使えるようになります。

var ins = new ClassD();
ins.alertWord(word);  // ClassA から継承
ins.alert1();               // ClassA から継承
// ins.alert2();           // ClassA から継承されない
ins.alert3();               // ClassA から継承
ins.test();                 // ClassD で実装

ここで、ins.alert2() は使えません

スーパークラスである ClassA alert2() メソッドは、コンストラクタ内で作成せずに、prototype で作成しました。

スーパークラスのプロトタイプ プロパティは、call や apply で行うクラスの継承では、サブクラスに引き継がれることがないんですね

試しに、前述の enumAllPrototypes 関数を使って、ClassD のクラス prototype を列挙してみましょう。

これで alert(enumPrototypes(ClassA)); とやると、結果がこうなります。
alert2 = function () { this.alertWord('ClassA 2'); }

一方、 alert(enumPrototypes(ClassD)); とやると・・・・・・、

 

空文字列が出力されます・・・・・・つまり、継承されてないんです。

そんなわけで、スーパークラスである ClassA にプロトタイプで動的に追加された alert2() メソッドは、ClassA クラス インスタンスでは使えるけれど、call apply で作成した ClassA の派生クラスである ClassD クラス インスタンスには引き継がれず、使うことができません。

この方法で ClassD クラス インスタンスから alert2() メソッドを使おうとすると、ブラウザが理解できずに、スクリプトの実行が止まります。

ちなみに。

callapply で、サブクラス ClassD で継承できるスーパークラス ClassA のプロパティは、ClassA のインスタンス プロパティだけですが、これらはみな ClassDインスタンス プロパティとして継承されます(プロトタイプ プロパティではありません)

インスタンス プロパティは「値」ですから、スーパークラスのインスタンス プロパティを動的に変化させても、サブクラスのインスタンス プロパティには影響しません!

あたりまえですね。

ここは、前述のプロトタイプによる継承とは、大きく異なる部分です。 


それならさ。

関数作っちゃえばいいじゃん。

スーパークラスとサブクラスがわかってるんだから、プロトタイプ代入で全部引き継いじゃいましょう。

// クラスのひとつの 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 関数に、サブクラスとスーパークラスを喰わせれば、スーパークラスからサブクラスに対して、プロトタイプを全部継承することができます

callapply でサブクラスを作ると、スーパークラスからのプロトタイプ継承はできませんが、事後のサポートとしてこの関数を呼び出せば、スーパークラスからサブクラスにプロトタイプを全部継承できます。

さっきの ClassD を流用すると、こんな感じです。

// サブクラス定義
function ClassD() {
    ClassA.call(this);    // インスタンス継承
    this.test = function () {
        this.alert1();
        this.alert2(); // ←使える
        this.alert3();
    }
    return this;
}
ClassD.prototype.constructor = ClassD;
inheritAllPrototypes(ClassD, ClassA);  // プロトタイプ継承

この方法が、前出の prototype new でスーパークラスのインスタンスをぶち込む方法と異なる点は、スーパークラスのインスタンス プロパティはサブクラスのインスタンス プロパティで、スーパークラスのプロトタイプ プロパティはサブクラスのプロトタイプ プロパティで継承できる点にあります。


ところが。

前出の prototype にスーパークラスのインスタンスをぶち込む方法と、上記の inheritAllPrototypes 関数でプロトタイプ プロパティを代入しまくる方法では、決定的な違いがあります

それは、スーパークラスのプロトタイプ プロパティを動的に変化させた場合の、サブクラスのプロトタイプ プロパティの振る舞いです。

まずは、inheritAllPrototypes 関数でプロトタイプ プロパティを代入しまくる方法で、ClassA のサブクラス(孫)を作成し、スーパークラスのプロトタイプ プロパティを動的に変化させて、サブクラスにその変更が反映されるか見てみます。

// ひとつめのサブクラス(子)
function ClassA_Inherit1() {
    ClassA.call(this);
    return this;
}
ClassA_Inherit1.prototype.constructor = ClassA_Inherit1;
// プロトタイプ継承
inheritAllPrototypes(ClassA_Inherit1, ClassA);

// ふたつめのサブクラス(孫)
function ClassA_Inherit2() {
    ClassA_Inherit1.call(this);
    return this;
}
ClassA_Inherit2.prototype.constructor = ClassA_Inherit2;
// プロトタイプ継承
inheritAllPrototypes(ClassA_Inherit2, ClassA_Inherit1);

// スーパークラス ClassA のプロトタイプ プロパティを変更

ClassA.prototype.alert2 = function () { alert('ClassA 2 Changed'); }

// 孫サブクラスのプロトタイプ プロパティはどうなる?
alert(enumAllPrototypes(ClassA_Inherit2));

この結果は、以下の通りです。

alert2 = function () { this.alertWord("ClassA 2"); }

変化なしです。

inheritAllPrototypes 関数でプロトタイプ プロパティを代入しまくる方法では、スーパークラス(ClassA)のプロトタイプの動的な変更は、サブクラス(孫)に動的に反映されません

これは、inheritAllPrototypes 関数の動作が、その時点でのスーパークラスのプロトタイプ プロパティの「コピー」をサブクラスに渡しているだけ・・・・・・ということを示しています。

せっかくのプロトタイプ プロパティの魅力が半減ですね。

次に、prototype にスーパークラスのインスタンスをぶち込む方法で、ClassA のサブクラス(孫)を作成し、スーパークラスのプロトタイプ プロパティを動的に変化させて、サブクラスにその変更が反映されるか見てみます。 

// ひとつめのサブクラス(子)
function ClassA_PrototypeInherit1() {
    return this;
}
ClassA_PrototypeInherit1.prototype = new ClassA();
ClassA_PrototypeInherit1.prototype.constructor = ClassA_PrototypeInherit1;

// ふたつめのサブクラス(孫)
function ClassA_PrototypeInherit2() {
    return this;
}
ClassA_PrototypeInherit2.prototype = new ClassA_PrototypeInherit1();
ClassA_PrototypeInherit2.prototype.constructor = ClassA_PrototypeInherit2;

// スーパークラス ClassA のプロトタイプ プロパティを変更
ClassA.prototype.alert2 = function () { alert('ClassA 2 Changed'); }

// 孫サブクラスのプロトタイプ プロパティはどうなる?
alert(enumAllPrototypes(ClassA_PrototypeInherit2));

この結果は、以下の通りです。

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

変化しています。

prototype にスーパークラスのインスタンスをぶち込む方法では、スーパークラス(ClassA)のプロトタイプの動的な変更は、サブクラス(孫)に動的に反映されます

これは、このサブクラスが、きちんとスーパークラスのプロトタイプ プロパティの「参照」を持っていることを示しています。

世間では、この仕組みを「プロトタイプチェーン」って言うらしいんですけどね。

不勉強な俺には、よくわかりませんwww。

「継承」という視点で見てみると、 prototype にスーパークラスのインスタンスをぶち込む方法のほうが、挙動としては妥当です。

inheritAllPrototypes 関数でプロトタイプ プロパティを代入しまくる方法は、非常に限定されたケースでの「逃げ」の手段にしかならないですね。


【追記】 2010/12/23/ 22:50

そこで。

思いつきなんですが。

「こんなのもいけるんじゃない?」という方法を試してみましょう。

インスタンス プロパティはインスタンス プロパティとして、プロトタイプ プロパティはプロトタイプ プロパティとして、しかもスーパークラスの動的なプロパティ インスタンスの変化をサブクラスに反映させる方法です。

prototype にスーパークラスのインスタンスをぶち込む方法と、call メソッドや apply メソッドを使う方法を併用してみます。

以下が、そのサンプル。 

// ひとつめのサブクラス(子)
function ClassA_Child() {
    ClassA.call(this);  //インスタンス継承
   return this;
}
ClassA_
Child.prototype = new ClassA();  // プロトタイプ継承
ClassA_
Child.prototype.constructor = ClassA_Child;

// ふたつめのサブクラス(孫)
function ClassA_GrandChild() {
    ClassA_Child.call(this);  //インスタンス継承
    return this;
}
ClassA_
GrandChild.prototype = new ClassA_Child();  // プロトタイプ継承
ClassA_GrandChild.prototype.constructor = ClassA_GrandChild;

// スーパークラス ClassA のプロトタイプ プロパティを変更
ClassA.prototype.alert2 = function () { alert('ClassA 2 Changed'); }

// 孫サブクラスのプロトタイプ プロパティはどうなる?
alert(enumAllPrototypes(ClassA_GrandChild));

この結果は、以下の通りです。

alertWord = function (word) { alert(word);}
alert1 = function () { this.alertWord("ClassA 1"); }
alert3 = function () { this.alertWord("ClassA 3");}
constructor = function ClassA_GrandChild() {
    ClassA_Child.call(this);
    return this;
}
alert2 = function () { alert("ClassA 2 Changed"); }

いけてんじゃん!

ちゃんと孫サブクラスのプロトタイプ プロパティが変化してんじゃん!

これなら、基本クラスのインスタンス プロパティとプロトタイプ プロパティの区別も(優先順位があるおかげで)崩れないしね。

ただし、ひとつのクラスの中に、同名のインスタンス プロパティとプロトタイプ プロパティが混在すると、インスタンス プロパティが先に解決される(=プロトタイプ プロパティが死ぬ)点には、気をつけないといけませんね。

これが一番まっとうな方法かな? 


・・・・・・ところでさ。

重箱の隅をつつくようで、申し訳ないけど。

call とか apply は、戻り値としてなにを返してくるの?

これを実験してみました。

// サブクラス定義
function TestInheritClassCall() {
    alert(ClassA.call(this));       // ff のみ object Object
    //enumAllProperties(ClassA.call(this));
    // ff のみ ClassA 1 と表示
}
TestInheritClassCall.prototype.constructor = TestInheritClassCall;

// サブクラスのインスタンス作成
var ins = new TestInheritClassCall();
// サブクラス定義
function TestInheritClassApply() {
    alert(ClassA.call(this));       // ff のみ object Object
    //enumAllProperties(ClassA.apply(this, arguments));
    // ff のみ object Object

    //enumAllProperties(enumAllProperties(ClassA.apply(this, arguments)));
    // ff のみ ClassA 1 と表示
}
TestInheritClassApply.prototype.constructor = TestInheritClassApply;

// サブクラスのインスタンス作成
var ins = new TestInheritClassApply();

困ったことに、これもブラウザ依存なんですよねえ・・・・・・。

Firefox 以外のブラウザでは、CallApply では、undefined を返してきます。

Firefox では、CallApply では、[object Object] を返してきます。

しかも、Firefox では、Call と Apply で返してくる Object  が違うんですね。

Firefox の call では、すぐに解決できる Object を返してきます。

一方 Apply では、解決に2段階を要する Object を返してきます。

まあ、callapply の戻り値を使うことは滅多にないだろうから、いいんだけどさ。


ここまでに、4つの方法を提示しました。

  1. プロトタイプにスーパークラスのクラス インスタンスをぶち込む方法(すべてプロトタイプ プロパティで継承)
  2. call や apply を使う方法(スーパークラスのインスタンス プロパティのみを継承)
  3. call や apply を使いながら、スーパークラスのプロトタイプもサブクラスに継承する方法(ただしプロトタイプ プロパティの魅力が半減)
  4. プロトタイプにスーパークラスのクラス インスタンスをぶち込む方法と、call や apply を使う方法の併用(スーパークラスのプロトタイプの動的な変化が、基本クラスに反映される)

ご理解頂けたでしょうか。

ここまでで、一応「クラスの作成」「クラスの継承」ができました。

「クラスの継承」を使っていけば、孫クラスや、ひ孫クラスもできます。

でも、ちょっと待って。

「クラスの継承」は、これだけじゃ終わらないんですよ・・・・・・。

C++の経験者なら生唾ものの、「クラスの多重継承(の、ようなもの)」も、実現できてしまうんです!

次回は、もう一回だけ、「クラスの継承」関連のエントリーを書きます・・・・・・「クラスの多重継承(の、ようなもの)です。


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

nice! 1

コメント 0

コメントを書く

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

トラックバック 0

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