- クラスの落とし穴1 - プロパティの初期化
- クラスの落とし穴2 - メソッドとクロージャ
- クラスの落とし穴3 - 継承
- クラスの落とし穴4 - プライベート変数の実装(この投稿)
はじめに
今回は、Object.defineProperty
を使ってプライベート変数を定義した場合の落とし穴です。
プライベート変数を利用するプロパティ・メソッドを実装するにはどうしたら良いかを考えます。
そこにも、落とし穴が存在しました。
動作確認はnodeで行っていますので、モダンブラウザ以外では動作しない場合があります
今回は実験コードです。実務での使用には慎重になってください。
プロパティ
まず、普通のプロパティとの違いと実装方法を説明します。
function Klass() {
};
var k = new Klass();
k.name = 'foo';
k.name = 123; // 本当は文字列だけ受け取りたいんだけど。。。
実装と言っても、特に何をするわけでもなく利用時に代入すれば良いだけです。
そのためnameにどのような型の値が設定されるかは利用者任せにされます。
Object.defineProperty
Object.defineProperty
では値の設定時や取得時に処理を記述する事が出来ます。
せっかく定義するので、型制限をつけてみましょう。
function Klass() {
};
Object.defineProperty(Klass.prototype, 'name', {
enumerable: true,
set: function (value) {
if (typeof value === 'string') {
this.name_ = value;
} else {
throw new TypeError('文字列ではありません');
}
},
get: function () {
return this.name_;
}
});
var k = new Klass();
k.name = 'foo';
k.name = 123; // 例外発生
k.name_ = 123; // でも直接設定できてしまう
文字列以外の設定時に例外を発生させる事が可能になりました。
ただし、実際の値が格納されているname_
というプロパティが丸見えです。
今回は、このname_
をプライベート変数として定義してみます。
プライベート変数を定義
さて早速コードを。
次の実装でname_
というプライベート変数を定義できました
Object.defineProperty
の第三引数は複雑そうに見えて、即時関数がenumerable
,get
,set
を持ったオブジェクトを返しているだけです。
それ以外はほとんど先ほどと変わりませんね。
function Klass() {
};
Object.defineProperty(Klass.prototype, 'name', (function () {
var name_ = null; // プライベート変数
return {
enumerable: true,
set: function (value) {
if (typeof value === 'string') {
name_ = value;
} else {
throw new TypeError('文字列ではありません');
}
},
get: function () {
return name_;
}
};
})());
var k = new Klass();
k.name = 'foo';
console.log(k.name);
k.name = 123; // 例外が発生
うまく動作していますね。
落とし穴はどこ?
しかし、今回も最初の例は落とし穴が存在します。
すぐに気がついた人は、prototypeを完全に自分のものにしていますね。
落とし穴をみつけましょう。
var k1 = new Klass();
var k2 = new Klass();
k1.name = 'foo';
k2.name = 'bar';
console.log(k1.name); // barと表示されてしまう
console.log(k2.name);
二つのインスタンスを作成し、それぞれname
を変更してみます。
しかし、あとに設定したものに2つとも変更されています。
実は、prototypeに設定した事でname_
がクラス全体で使用されるstaticな変数になってしまいました。
これはいただけません。
しかもname_
はname
のGetter/Setterからしかアクセスできませんので、使い道が乏しくなります
インスタンスのプライベート変数を定義
コンストラクタ内でプライベート変数を設定するのはどうでしょう。
これならインスタンス毎にプライベート変数を定義出来るはずですし、複数のObject.defineProperty
を定義しても共有できるようになります。
そこで次のように実装しました。
function Klass() {
var name_ = null;
Object.defineProperty(this, 'name', {
enumerable: true,
set: function (value) {
if (typeof value === 'string') {
name_ = value;
} else {
throw new TypeError('文字列ではありません');
}
},
get: function () {
return name_;
}
});
};
これは、たしかにうまい方法かも知れません。
でもクラスの落とし穴2 - メソッドとクロージャの問題が発生します。
つまり、このままではメモリーリークの危険性が高くなっています。
クラスの利用者を100%信じる事ができないならコンストラクタの中にインスタンスと同じライフサイクルをもつ関数はない方が良い事を覚えて置いてください。
その2
次は、コンストラクタから関数を追放したパターンです。
function Klass() {
var pv = {}; // プライベート変数を格納するオブジェクト
Object.defineProperty(this, 'name', getNameProperty(pv));
};
//定義を返す関数
function getNameProperty (pv) {
return {
enumerable: true,
set: function (value) {
if (typeof value === 'string') {
pv.name = value;
} else {
throw new TypeError('文字列ではありません');
}
},
get: function () {
return pv.name;
}
};
};
クラスの落とし穴2 - メソッドとクロージャの問題を回避し、プライベート変数を定義できました。
ポイントは、コンストラクタでプライベート変数を格納するオブジェクトを作成することです。
メソッドの定義
メソッドの定義も簡単です。
function Klass() {
var pv = {}; // プライベート変数を格納するオブジェクト
Object.defineProperty(this, 'getName', getNameMethod(pv));
};
//定義を返す関数
function getNameMethod (pv) {
return {
value: function () {
return pv.name;
}
};
};
プライベート変数をメソッドから使用するには、{value: Function}
を設定します。
プロパティではenumerable:true
があり、メソッドで省かれている理由はfor-in
やObject.keys
のためです。
クライアントサイドでの注意点
今回のコードをクライアントサイドで利用する場合はgetNameProperty
とgetNameMethod
がグローバル関数になってしまいます。
クラス定義全体を即時関数でくくってKlassを戻り値にする必要がありますが、それは説明していません。
さいごに
javascriptでプライベート変数を実装するなら自然とクロージャを利用するしかありません。
そのためprototypeの恩恵を得る事ができなくなります。メソッドをインスタンスに直接設定する必要が出てくるのは問題がありそうですね。
javascriptのクラスにはプライベート変数を無理に実装しなくてもいいのでは? と思っています。
しかし、どうしてもカプセル化をしたいのであれば今回の実験コードが役にたつかもしれません。