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
当然、this
は obj
である。
では、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]]の実行となる。
- [[Get]]
- Proxy経由なので handler 内の
target[name]
から [[Get] が実行される - getName プロパティは DataDescriptor であるので、value 値を返す
- Proxy経由なので handler 内の
- [[Call]]
- proxyObj.getName でプロパティを得ているので、thisArgument が proxyObj に設定される
name getter の場合
- [[Get]]
- Proxy経由なので handler 内の
target[name]
から [[Get] が実行される - name プロパティは AccessorDescriptor であるので、desc.get を実行する
- この時、
target[name]
からのアクセスであるため、desc.get の thisArgument は target、つまり obj に設置される
- この時、
- Proxy経由なので handler 内の
というわけで、仕様通りに動いた結果だと分かる
矯正
仕様通りとはいえ、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のthisArgumentをreceiverにしてくれるみたい。
こうすることで、this を統一して proxyObj に矯正できる。
めでたしめでたし(?)
*1:バグと書いたけどバグではない