ES6 の機能を使って class における private プロパティ相当を実現しようという話。
割とググるとゴロゴロ出てくる情報だが日本語だとあまりないっぽいし、自分が覚えておくためにも書いておく。
Symbol を使う方法
Symbol は同じものを作れない性質を利用して実現する:
const privateName = Symbol('name');
export default class Person {
constructor(name) {
this[privateName] = name;
}
greet() {
console.log(`Hello, I am ${this[nameSym]}`);
}
}
外部相当:
import Person from './person';
let p = new Person('John');
p.greet();
// Hello, I'm John
// .name で参照しても
console.log(p.name);
// undefined
// Symbol('name') で参照しても、
console.log(p[Symbol('name')]);
// undefined
実行は babel 経由で:
$ node -e 'require("babel/register");require("./main");'
Hello, I am John
undefined
undefined
もっと簡単に実行できないの。
追記: Symbol を使った例は private ではない
WeakMap を使う方法
外からアクセスできない WeakMap を定義して、プライベートプロパティを実現する:
const privateMap = new WeakMap();
function getPrivates(self) {
let p = privateMap.get(self);
if (!p) {
p = {};
privateMap.set(self, p);
}
return p;
}
export default class Person {
constructor(name) {
getPrivates(this).name = name;
}
greet() {
console.log(`Hello, I am ${getPrivates(this).name}`);
}
}
外部相当は Symbol の例と同じコードなので省略。
実行も同様に babel で:
$ node -e 'require("babel/register");require("./main");'
Hello, I am John
undefined
undefined
「なぜ WeakMap なのか Map じゃダメなのか」って話だけど、Map だと this
がいつまでたっても GC されなくなってしまうのでダメ。
所感と補足
Symbol のほうが記述量少ないし、読み書きしやすいので好み。
しかし、場合によっては WeakMap のほうが記述量が少なくなるケースがある。(例えば次の話)
JSON.stringify
/ JSON.parse
との連携
2つの方法両方共に、プライベートメンバは JSON.stringify
には載らない:
import Person from './person';
let p = new Person('John');
console.log(JSON.stringify(p));
// {}
そもそも class と JSON.parse
はそのままだとあまり相性良くないので、上手く書く必要がある:
// person.js
// WeakMap を使った例から改変
// ...
export default class Person {
constructor(name) {
getPrivates(this).name = name;
}
greet() {
console.log(`Hello, I am ${getPrivates(this).name}`);
}
// JSON 文字列にする
toJson() {
return JSON.stringify(getPrivates(this));
}
// JSON 文字列から変換
static fromJson(jsonString) {
const p = new Person();
privateMap.set(p, JSON.parse(jsonString));
return p;
}
}
// main.js
import Person from './person';
let p = Person.fromJson('{ "name": "John" }');
p.greet();
// Hello, I am John
console.log(p.toJson());
// {"name":"John"}
この時は WeakMap でプライベートプロパティを実現すると楽なのが分かる。
追記: Symbol を使った例は private ではない
この記事のリアクションとして @teramako さんから指摘がありました:
何度でもしつこく言うよ。Symbolはユニークだがプライベートなプロパティではない。
https://twitter.com/teramako/status/574782371167240194
これはつまり、Symbol を使った方法はインスタンス外からアクセスできてしまうケースがあるということ。
同一ファイル内なら簡単にアクセスできてしまう:
const privateName = Symbol('name');
export default class Person {
constructor(name) {
this[privateName] = name;
}
greet() {
console.log(`Hello, I am ${this[nameSym]}`);
}
}
let p = new Person('John');
console.log(p[privateName]);
// John
外からでも Object.getOwnPropertySymbols
を使えば:
import Person from './person';
let p = new Person('john');
Object.getOwnPropertySymbols(p).forEach(s => {
console.log('%s: %s', s.toString(), p[s]);
});
$ node -e 'require("babel/register");require("./main");'
Hello, I am john
Symbol(name): john
後者の例に関しては、特定プロパティへのアクセス難易度が上がっているためアクセス制限できている、と言って良いが、前者の性質は理解した上で使う必要がある。
この性質から Symbol を使った方法は private ではない別のカプセリング的な何か 程度が正しい認識になる。
ご指摘ありがとうございました。
更に追記: WeakMap
を使ったケースも同一ファイルからならアクセスできますね。