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

  • 41
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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 経由で:

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

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

追記: 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 で:

端末
$ 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
// 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 を使った方法はインスタンス外からアクセスできてしまうケースがあるということ。

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

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]);
});
端末
$ node -e 'require("babel/register");require("./main");'
Hello, I am john
Symbol(name): john

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

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

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

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