JavaScriptでprivateなプロパティやメソッド(以下メンバ)を定義する方法を書いていく。
ES6未満では、privateなメンバを定義するために色々な工夫が重ねられていた。それでも全くprivateなプロパティを作るのは難しかった。しかし、ES6ではWeakMapというオブジェクトが使えるようになり、完全にprivateなメンバを定義できるようになった。
ここではまず、ES6未満でprivateなメンバを定義するための工夫の数々を紹介し、その後、WeakMapを使ったprivateなメンバを定義する方法を書く。そして、最後にES6でのClass定義の自分の書き方パターンを紹介したい。
ES6未満でのprivateなメンバの定義方法
まず、1つの工夫としてprivateなメンバには先頭に_(アンダーバー)をつけるという方法があるが、これは外部からアクセスできるためprivateでもなんでもない。
もう1つの方法としてメンバを全てconstructor内に記述するという方法もある。
function Foo() { var privateProp = 1; this.method = function() { return privateProp++; }; }
これは確かにprivateなメンバを作れるが、PrototypeベースというJavaScriptの利点をなくしてしまう他、インスタンス化する度にメソッドを定義するためメモリも余計に食う。
現実的な方法としては、以下のような書き方がある。
var Foo = (function() { var privates = {}; var privateId = 0; function Foo() { Object.defineProperty(this, '_id', { value: privateId++ }); privates[this._id] = {}; privates[this._id].prop = 1; } Foo.prototype.method = function() { return privates[this._id].prop }; return Foo; })();
この方法はthis._idというプロパティを作ってしまうことさえ許容できれば、かなりgoodな方法である。しかし、WeakMapを使えばこのようなプロパティを作る必要もなくなる。
ES6のWeakMapを使ってprivateなメンバを定義する
まず、WeakMapを知らない方へWeakMapとは何なのか簡単に説明するが、普通のObjectはkeyに文字列を使うのに対して、WeakMapはkeyにオブジェクトを使う。つまり、プリミティブ型でなければObjectでもArrayでもFunctionでも何でもkeyにできる
var a = {}; var b = []; var c = () => {}; var weakmap = new WeakMap(); weakmap.set(a, 1); weakmap.set(b, 2); weakmap.set(c, 3); console.log(weakmap.get(b)); // -> 2;
このようにオブジェクトをkeyとして使えるため、Classをインスタンス化する際、そのインスタンス(this)をWeakMapにsetすればWeakMap.get(this)でメンバにアクセスできるのだ。百聞は一見にしかずなので例示する。
var Foo = (function() { var privates = new WeakMap(); function Foo() { privates.set(this, {}); privates.get(this).prop = 1; } Foo.prototype.method = function() { return privates.get(this).prop }; return Foo; })();
ES6のClass定義パターン
上のように、WeakMapの中のプロパティにアクセスするためにはgetやsetというメソッドを使う必要があり、それは面倒なので以下のようなnamespaceという関数を作る。
'use strict'; export default function namespace() { const map = new WeakMap(); return function(object) { if(! map.has(object)) { map.set(object, {}); } return map.get(object); }; };
これをClassファイルにimportしてprivateなメンバを定義する方法を以下に例示する。
'use strict'; import ns from './namespace'; const privates = ns(); class Foo { constructor() { const self = privates(this); self.privateProp = 1; this.prop = 2; self.privateMethod1 = privateMethod1.bind(this); self.privateMethod2 = privateMethod2.bind(this); } method() { const self = privates(this); return self.privateProp + self.privateMethod1(); } } function privateMethod1() { const self = privates(this); return self.privateProp + this.prop; } function privateMethod2() { return this.method(); }
ここでは、privates(this)で返ってくるオブジェクトにprivateなメンバをセットする。取得するときもprivates(this)から取得できる。privates(this)をいちいち呼び出すより、selfなどの変数に入れておくほうがよい。
また、privateなメソッドはclassの外でfunction文で宣言して(ホイスティング)、constructor内でprivates(this)にthisをbindして代入したほうがネストも浅くて済む。
privateなメンバのアクセスをどこまで許すかは即時関数(ブロック)で囲うか囲わないかによる。すぐ上の例のようにファイル全体まで許すこともできる。
また、browserifyなどを使って複数のファイルを1つのファイルにあらかじめコンパイルしているのであれば、それらのファイル内のみメンバのアクセスを許可するといったような名前空間を作ることも可能である。
import ns from ./namespace; export default ns();
import internal from './internal' export default class Foo { conctructor() { internal(this).prop = 1; } }
import internal from './internal' import Foo from ./Foo const foo = new Foo(); console.log(internal(foo).prop); // -> 1;
1つの方法として覚えておくと使える時がくるかもしれない。
終わりに
今回は、JavaScriptでprivateなメンバを定義する方法を書いてきた。特に、WeakMapを使った方法はかなり有用性があり、途中で示したnamespace.jsと合わせて使うとJavaScriptでもprivateなメンバを簡単に書けるようになる。今後、デフォでJavaScriptでもprivateなメンバを定義できるようになるかもしれないが、それまでは、このWeakMapを使った方法で定義することを推奨したい。