ECMAScript Proxy の get 時の注意点

DirectProxyになる前に上げたバグ*1なのでちょっとコードが古いけど、回答が付いて気付かされた。

Proxy通過時の this の問題

var obj, handler, proxyObj;
obj = {
  _name: "HOGE",
  getName: function () {
    console.log("getName()", "this is obj:", this === obj, "this is proxyObj:", this === proxyObj);
    return this._name;
  },
  get name() {
    console.log("get name", "this is obj:", this === obj, "this is proxyObj:", this === proxyObj);
    return this._name;
  },
};

handler = {
  get: function (target, name) {
    console.log("[Handler]get", name);
    return target[name];
  },
};

proxyObj = new Proxy(obj, handler);
obj.getName(); // => "HOGE"
// console: getName() this is obj: true this is proxyObj: false

obj.name; // = "HOGE"
// console: get name this is obj: true this is proxyObj: false

当然、thisobj である。

では、proxyを通すと...

proxyObj.getName(); // => "HOGE"
// console: [Handler]get getName
// console: getName() this is obj: false this is proxyObj: true 
// console: [Handler]get _name

proxyObj.name; // = "HOGE"
// console: [Handler]get name
// console: get name this is obj: true this is proxyObj: false
  • getName() メソッドでは this が proxyObj
  • name のgetter では this が obj

となる。おや? となるのが、ボクが上げたバグ。

が、ECMAScript 仕様のプロパティを得る内部メソッド[[Get]]を追うと謎が解けて、これはバグではなく仕様であると納得できる。

仕組み

getName() メソッドの場合

これ、正確には2段階に分けられ、[[Get]]によるプロパティ取得と、[[Call]]の実行となる。

  1. [[Get]]
    • Proxy経由なので handler 内の target[name] から [[Get] が実行される
    • getName プロパティは DataDescriptor であるので、value 値を返す
  2. [[Call]]
    • proxyObj.getName でプロパティを得ているので、thisArgument が proxyObj に設定される
name getter の場合
  1. [[Get]]
    • Proxy経由なので handler 内の target[name] から [[Get] が実行される
    • name プロパティは AccessorDescriptor であるので、desc.get を実行する
      • この時、target[name] からのアクセスであるため、desc.get の thisArgument は target、つまり obj に設置される

というわけで、仕様通りに動いた結果だと分かる

矯正

仕様通りとはいえ、this が異なってしまうのはどうにもやりにくい。
ということで、ECMAScript 6th の Reflect を使用する。(まだ実装されてないけど)

handler = {
  get: function (target, name, reciever) {
    console.log("[Handler]get", name);
    return Reflect.get(target, name, reciever);
  }
};
// or
handler = {
  get: function (target, name, reciever) {
    console.log("[Handler]get", name);
    var desc = Object.getOwnPropertyDescriptor(target, name);
    // 本当は desc === undefined の場合に prototype チェーンを辿る必要があるけど、割愛
    if ("value" in desc)
      return desc.value;
    else if (desc.get)
      return desc.get.call(reciever);
    else
      return undefined;
  }
};

とする。reciever はこの場合、proxyObj がくる。これで Reflect.get をすると、AccessorDescriptor(getter/setter)の場合にcallするget/setのthisArgumentreceiverにしてくれるみたい。

こうすることで、this を統一して proxyObj に矯正できる。

めでたしめでたし(?)

*1:バグと書いたけどバグではない