LoginSignup
197
201

More than 5 years have passed since last update.

クラスの落とし穴1 - プロパティの初期化

Last updated at Posted at 2013-10-05

はじめに

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']

inst2hobbiesには、joggingだけではなくguitarも追加されています。
じつは、prototypeに追加されたプロパティはnewで作成されたオブジェクトから参照する事ができるだけでなく、どの(OOでいう)インスタンスからも変更できます。
そのため、 inst1inst2が参照している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がメモリ節約をする利点が生かされなくなるからです。
特に共通の処理であるメソッドをコンストラクタで設定することは、また別のある落とし穴が存在します。

さいごに

プロパティの実装はコンストラクタで行いましょう これだけ覚えておけば今回は大丈夫です
まだまだ落とし穴がたくさんありますので、他の投稿も確認していただければと思います。

197
201
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
197
201