ES Harmony の Proxy と WeakMap を使ってプライベート変数(プロパティ)を作る

Firefox 6.0a1 の話。なんか、FirefoxってECMAScript.nextの実験台になっている気が...w

まず、今のJavaScript

  • JavaScriptにはプライベート変数がない。
    • クロージャを使えばできなくはないけど、完璧じゃない。
  • 習慣的にプライベートなプロパティには "_" を先頭に付けている。
    • とはいえ、他からアクセス可能
var o = {
  _name: "Foo",
  getName: function () {
    return this._name;
  }
};

_nameはプライベートなプロパティとしているのに、o._nameでアクセスできちゃう。こんなのプライベートじゃないやい、ってわけですよ。

んで、Proxyを使って、どうにかできないものかと考えた。他からアクセスした時は undefined を返して、自身の関数からthis._nameみたいにアクセスした時には値を返すというものを作ってみた。

// Proxyハンドラとなるprototype
var ProxyHandler = {
  get: function (_, name) {
    if (name[0] == "_") {
      var caller = arguments.callee.caller; //呼び出し元関数を得る
      if (!caller)
        return void(0);

      var id = this.vm.get(caller);
      if (this.ID != id)
        return void(0);

      return this.vm.get(this.target)[name];
    }
    return this.target[name];
  },
  set: function (_, name, value) {
    if (name[0] == "_") {
      var caller = arguments.callee.caller; //呼び出し元関数を得る
      if (!caller)
        return false;

      var id = this.vm.get(caller); // WeakMapから呼び出し元関数のid値を得る
      if (this.ID != id)
        return false;

      this.vm.get(this.target)[name] = value;
      return true;
    }
    this.target[name] = value;
    return true;
  },
  // 他のハンドラは省略
  // ...
}
function createProxy (target) {
  const id = "ID_" + Date.now(); // 本当はUUIDみたいのが良いね!
  var handler = Object.create(ProxyHandler, {
    target: { value: target },
    vm: { value: new WeakMap() },
    ID: { value: id },
  });
  // target に対応するプライベートプロパティを入れる箱を作る
  handler.vm.set(target, {});

  var keys = Object.getOwnPropertyNames(target);
  for (var i = 0, len = keys.length; i < len; ++i) {
    var key = keys[i],
        desc = Object.getOwnPropertyDescriptor(target, key);
    // 関数、gettter, setter に対応する id をセット
    if (desc.value && typeof desc.value == "function") {
      handler.vm.set(desc.value, id);
    } else if (desc.get) {
      handler.vm.set(desc.get, id);
      if (desc.set)
        handler.vm.set(desc.set, id);
    }
  }
  return Proxy.create(handler, Object.getPrototypeOf(target));
}

var o = {
  setName: function (name) {
    return this._name = name;
  },
  getName: function () {
    return this._name;
  },
  get self () {
    return this;
  }
};
var proxyObject = createProxy(o);

console.log("proxyObject.setName('HOGE') =>", proxyObject.setName("HOGE"));
console.log("proxyObject._name =>", proxyObject._name); // undefined
console.log("proxyObject.getName() =>", proxyObject.getName()); // "HOGE"
console.log("proxyObject.self === proxyObject =>", proxyObject.self === proxyObject);
console.log("proxyObject.self === o =>", proxyObject.self === o);

で、なにやっているかというと。
Proxyオブジェクト作成時に

  • 一意なIDを作成
  • Proxyのハンドラを作成
    • 作成したID, 対象オブジェクト, WeakMapを設置
  • 対象オブジェクト内の関数, gettter, setter をキー、作ったIDをvalueにWeakMapにセット
  • get時、set時にプロパティ名の先頭が "_" だったら
    • 実行元関数オブジェクトを取る
    • 関数オブジェクトに対応するIDをWeakMapから取る
    • そのIDが対象オブジェクトの持つIDと一致していたら、WeakMapから対象オブジェクトをキーにプライベートなプロパティをWeakMapから取る、または設置する
  • "_"から始まってなければ、普通に対象オブジェクトからプロパティをget/setする


ただし、getter/setter の場合、thisが作ったProxyオブジェクトとはならずに対象オブジェクトそのものになっているみたいで、getter/setter からはthis._nameをやってもProxyを経由しない模様。バグなのかどうかすらよく分からない。

因みに、Firefox 6.0a1 では Scratchpad (F4)なるものができていて、コードを一気に貼り付けて、実行(Ctrl+t)ができて便利!
http://gyazo.com/b4c4ade44383686097134b46fd4c5e21.png