- クラスの落とし穴1 - プロパティの初期化
- クラスの落とし穴2 - メソッドとクロージャ
- クラスの落とし穴3 - 継承(この投稿)
- クラスの落とし穴4 - プライベート変数の実装
#はじめに
今回はクラスの継承の違いによる落とし穴です。
継承とプロトタイプチェーン。同じようで同じじゃないやっぱりある落とし穴はなんでしょうか。
継承
javascriptはプロトタイプベースのため、PHPやjavaのように継承の方法に明確な構文がありません。
そのため、継承に似たものを工夫して実装することになります。
少なくとも継承にプロトタイプチェーンを使用すれば良いだろうというのは理解されていると思いますが、実際にはこれが一番の方法で後は全部だめ!というパターンはありません。
ただ、安易な実装は思っている動作とじつは異なる動作をしている事があります。
ここでは一番簡単な方法から継承を実装して確認します。
よくある継承の方法
// 親クラス
function Super (value) {
// プロパティ
this.prop1 = value;
};
// メソッド
Super.prototype.method1 = function method1 () {
console.log('method1');
};
// 子クラス
function Sub (name) {
// 親コンストラクタの呼び出し
Super.call(this, name);
// プロパティ
this.prop2 = 'bar';
};
// ■■■継承■■■必ず子クラスのメソッドの定義の前に記述してください
Sub.prototype = new Super();
// メソッド
Sub.prototype.method2 = function method2 () {
console.log('method2');
};
// 使用
var inst = new Sub('foo');
// プロパティ
console.log(inst.prop1); // fooと表示
console.log(inst.prop2); // barと表示
// メソッド
inst.method1(); // method1と表示
inst.method2(); // method2と表示
うまく動作しているようです。
継承はたった一行で実装しています。これだけで動作するならベストなんではないでしょうか!
さて
じつは今回もこの実装には決定的にまずいところが一つあります。
それが継承の落とし穴です。
さてそれがどのような理由なのか分かるでしょうか?
落とし穴はドコ?
それは、親クラスのコンストラクタにconsole.log('hoge')
などの一行を加えてみると分かります。
おそらく、hoge
が2回出力される事でしょう。
それはSub.prototype = new Super();
で1回目のコンストラクタが呼ばれ、さらにサブクラスから親クラスのコンストラクタが呼ばれている為です。
これがなぜマズいのか?例えば、こんなシナリオはどうでしょう
- 親クラスのコンストラクタで(websocketなど)ソケットオブジェクト作成
- 使用されないオブジェクトがサーバ等に接続をし、保持される
- その後、実際の接続に使用されるインスタンスが子クラスから作成される
- サーバ側ではブラウザがページを開く毎に2つのリスナーの作成が行われる
明らかにサーバに無駄な負担をかけています。
しかし、実際には動作に問題が起きている事に気がつきません。
親のコンストラクタはどのタイミングで呼ぶべきか?
じつは new Super()
で継承を行っても良い条件は、親クラスのコンストラクタにステートメントが存在しない事です。
ということは、その場合は継承時に呼ばれていも問題ありません。
親クラスの処理をすべて子クラスに移動させて。上記の処理はこのようにすべきです。(大嘘)
// 子クラス
function Sub (name) {
// プロパティ
this.prop1 = value;
// プロパティ
this.prop2 = 'bar';
};
え〜。。。本当ですか?
継承では共通処理をまとめることを目的としているので、 「子クラスのコンストラクタだけで親クラスの初期設定もしてください!」 というのはクラスの継承の利点が損なわれてしまいます。
そもそも、継承先のクラスすべてにprop1
を忘れずに記述するのはおかしいと思います。
その通りですね。
子クラスから親クラスのコンストラクタを呼び出すのを、生かしたいなら 継承の部分を変更するしかありません。
子クラスで親で設定していたプロパティを実装するのは根本的な解決にはなっていませんでした。
そもそも親クラスでだけで動作する事ができなくなっています。
これでは抽象クラスしか親クラスに慣れない不便な継承になってしまいます。
もはや継承でもなんでもなくなってしまいます。
では、ほかの継承の方法を探っていきましょう。
node.jsの継承をみてみよう
node.jsでは、継承をサポートするモジュールutilが実装されています。
util.inheritsを使用して継承し、使用方法は次の通りです
var util = require('util');
util.inherits(Sub, Super);
util.inherits
を使用した継承では、親クラスのコンストラクタが継承時に実行される事はありません。
実際はutil.inheritsが何を行っているのかこっそりのぞいてみましょう。
// util.jsから抜粋
exports.inherits = function(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
super_
は、inherits
が勝手に作成しているプロパティなので無視してかまいません。
次の行でObject.createを使用していますね。
これは、オブジェクトにプロパティを追加した新たなオブジェクトを返します。
クライアントサイドで継承
しかし、クライアントサイドでも使用しようとするとObject.inherits
はIE8などObject.createをサポートしないブラウザでは動作しない問題があります。
そこで、IE8でも動作する継承関数を作成してみます。
詳しい説明は省きますが、上記のutil.inherits
をObject.create
を使用しないで、記述すると次のようになります。
function inherits(sub, sup) {
sub.super_ = sup;
var F = function F () {};
F.prototype = sup.prototype;
sub.prototype = new F();
sub.prototype.constructor = sub;
};
Object.create
を使用したものと同じ動作をします
これでIE8でも動作する継承をサポートする関数を利用出来ます。
使用方法は次の通りです
inherits(Sub, Super);
継承に頼りすぎない
型指定のないjavascriptでは継承はあくまで共通処理をまとめるひとつの手段です。
動的にプロパティを変更する事が多いjavascriptでは親クラスへの操作が、全ての子クラスに影響を与えてします。
時にはmix-inの方がスマートな解決になるかもしれません。
時と場合により、出来るだけ簡素な方を採用して下さい。
さいごに
今回は、親クラスのコンストラクタに注目して動作を確認しました。
子クラスから親クラスのコンストラクタの呼び出しを行っている場合には、 安直な継承は危険である ことを理解できたと思います。