- クラスの落とし穴1 - プロパティの初期化 (この投稿)
- クラスの落とし穴2 - メソッドとクロージャ
- クラスの落とし穴3 - 継承
- クラスの落とし穴4 - プライベート変数の実装
はじめに
javascriptでもクラスを作成する事が多くなってきました。
しかし、「javascriptにはクラスがない」とよく言われ、実装側が擬似的にクラスを定義しています。
クライアントサイドで大規模開発が増えてきたため、オブジェクト指向の概念で実装したいと思っているのでしょう。
javascriptでは安易な疑似クラスの作成によって見事落とし穴にはまる事があります。
ここでは、本来javascriptにはないクラスをうまく実装する方法を順に追って説明します。
"落とし穴とは、 うまく動いているけど気がついていないだけで実はマズい実装の事 とします
簡単なクラスの実装
クラスの定義には幾つかの方法がありますが、多数派であるプロトタイプを使用を採用します。
// クラスを定義
function Klass () {};
// プロパティ
Klass.prototype.name = 'foo';
// メソッド
Klass.prototype.setName = function setName (value) {
this.name = value;
};
使用するには以下のようにします。
var instance = new Klass();
instance.setName('bar');
console.log(instance.name);
基本的にはこの様な記述でクラスのようなものが出来ました。
動作もうまく行っているようです。
間違った実装
次の例は先ほどとほとんど同じようなコードに見えて、クラスの機能としては確実に間違っているところがあります。
それがどのような理由かすぐにわかるでしょうか?
// クラスを定義
function Klass () {};
// プロパティ
Klass.prototype.hobbies = [];
// メソッド
Klass.prototype.addHobby = function addHobby (value) {
this.hobbies.push(value);
};
最初の例と変わらないように見えますので、うまく動作しそうです。
実際に使用してみても問題はないように見えます。
var instance = new Klass();
instance.addHobby('guitar');
console.log(instance.hobbies); // ['guitar']
しかし、次のコードを動作させるとよくわかります。
var inst1 = new Klass();
inst1.addHobby('guitar');
console.log(inst1.hobbies); // ['guitar']
var inst2 = new Klass();
inst2.addHobby('jogging');
console.log(inst2.hobbies); // ['guitar', 'jogging']
inst2
のhobbies
には、jogging
だけではなくguitar
も追加されています。
じつは、prototypeに追加されたプロパティはnew
で作成されたオブジェクトから参照する事ができるだけでなく、どの(OOでいう)インスタンスからも変更できます。
そのため、 inst1
とinst2
が参照しているhobbies
は同じ実体です。
これはプロトタイプチェーンによる参照による正常な動作です。
理解すると「なんだ単純なミスだ」と思うかもしれません。
しかし、この不具合の厄介なところは、 一つのインスタンスを作成しテストしただけでは全く間違いに気がつかない事です。
では最初のクラス定義はなぜうまく行ったのでしょうか?
それは、文字列は参照しているプロパティを変更したのはなく、 新たに値を設定した事 により参照がnewで作成されたオブジェクトのプロパティとして上書きされたため、たまたまうまく動作したにすぎません。
代入とarray.push
にプロトタイプチェーンでの参照時に大きな違いがあることがポイントです。
この事は、console.log(inst1)
でオブジェクトを出力するとよくわかります。
最初のsetName
を実行したインスタンスは{name: 'bar'}
と出力され、addHobby
を実行したインスタンスは{}
と出力されます。hobbies
プロパティが存在しません。
正しい実装方法は?
クラスを定義するのに定石となる実装方法はあるのでしょうか?
おそらく一番簡単な方法はコンストラクタで、すべてのプロパティに初期値を設定する事です。
最初の例と2番目の例のプロパティ・メソッドを一緒に定義して書き直すと次のようになります。
// クラスを定義
function Klass () {
// プロパティ
this.name = 'foo';
this.hobbies = [];
};
// メソッド
Klass.prototype.setName = function setName (value) {
this.name = value;
};
Klass.prototype.addHobby = function addHobby (value) {
this.hobbies.push(value);
};
しかし、メソッドや定数までコンストラクタで設定する必要はありません。prototypeがメモリ節約をする利点が生かされなくなるからです。
特に共通の処理であるメソッドをコンストラクタで設定することは、また別のある落とし穴が存在します。
さいごに
プロパティの実装はコンストラクタで行いましょう これだけ覚えておけば今回は大丈夫です
まだまだ落とし穴がたくさんありますので、他の投稿も確認していただければと思います。