ES6 class での private プロパティの定義

ES6 の機能を使って class における private プロパティ相当を実現しようという話。

割とググるとゴロゴロ出てくる情報だが日本語だとあまりないっぽいし、自分が覚えておくためにも書いておく。


Symbol を使う方法
-----------------

[Symbol][] は同じものを作れない性質を利用して実現する:

~~~~~~~~~~~~~~~~~~person.js
const privateName = Symbol('name');

export default class Person {
  constructor(name) {
    this[privateName] = name;
  }
  greet() {
    console.log(`Hello, I am ${this[nameSym]}`);
  }
}
~~~~~~~~~~~~~~~~~~~

外部相当:

~~~~~~~~~~~~~~~~~~~~~main.js
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 経由で:

~~~~~~~~~~~~~~~~~~~~~~sh:端末
$ node -e 'require("babel/register");require("./main");'
Hello, I am John
undefined
undefined
~~~~~~~~~~~~~~~~~~~~~~~~

もっと簡単に実行できないの。

追記: [Symbol を使った例は private ではない](#追記-symbol-を使った例は-private-ではない)

WeakMap を使う方法
-----------------

外からアクセスできない [WeakMap][] を定義して、プライベートプロパティを実現する:

~~~~~~~~~~~person.js
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 で:

~~~~~~~~~~~~~~~~~~~~~~sh:端末
$ 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` には載らない:

~~~~~~~~~~~~~~js
import Person from './person';
let p = new Person('John');
console.log(JSON.stringify(p));
// {}
~~~~~~~~~~~~~~~~~

そもそも class と `JSON.parse` はそのままだとあまり相性良くないので、上手く書く必要がある:

~~~~~~~~~~~~~~~~person.js
// 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](https://twitter.com/teramako) さんから指摘がありました:

> 何度でもしつこく言うよ。Symbolはユニークだがプライベートなプロパティではない。 
> https://twitter.com/teramako/status/574782371167240194

これはつまり、Symbol を使った方法はインスタンス外からアクセスできてしまうケースがあるということ。

同一ファイル内なら簡単にアクセスできてしまう:

~~~~~~~~~~~~~~person.js
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` を使えば:

~~~~~~~~~~~~~~~main.js
import Person from './person';
let p = new Person('john');
Object.getOwnPropertySymbols(p).forEach(s => {
  console.log('%s: %s', s.toString(), p[s]);
});
~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~sh:端末
$ node -e 'require("babel/register");require("./main");'
Hello, I am john
Symbol(name): john
~~~~~~~~~~~~~~~~~~

後者の例に関しては、特定プロパティへのアクセス難易度が上がっているためアクセス制限できている、と言って良いが、前者の性質は理解した上で使う必要がある。

この性質から __Symbol を使った方法は private ではない別のカプセリング的な何か__ 程度が正しい認識になる。

ご指摘ありがとうございました。

更に追記: `WeakMap` を使ったケースも同一ファイルからならアクセスできますね。

[Symbol]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
[WeakMap]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap