Proxy forwarding handlers and accessor properties
Le 17/06/2011 01:54, David Flanagan a écrit :
With ordinary objects, methods and property getter functions are both invoked on the object itself. If this object is proxied with Proxy.Handler, however, the property getter function will be invoked on the original object and the method will be invoked on the proxy object. The code below demonstrates.
I find this surprising, though I'm not sure whether it constitutes a bug in Proxy.Handler. I do think it is at least a reason why the receiver argument to get() is necessary and should not be removed as proposed by strawman:proxy_drop_receiver
The code that demonstrates this is below. It runs in the current Firefox Aurora. I have actually been affected by this issue when the proxy object is stored in a WeakMap but the object it is forwarding to is not in the WeakMap.
David Flanagan
if (this.console) print = console.log.bind(console);
// An object with an accessor property and a method. var o = { get property() { return map.get(this); }, method: function() { return map.get(this); } };
var map = new WeakMap(); map.set(o, "o");
// Both the getter function and method are invoked on o print(o.property); // prints "o" print(o.method()); // prints "o"
// This works for inherited properties and methods, too var p = Object.create(o); map.set(p, "p"); print(p.property); // prints "p" print(p.method()); // ditto
// When we create a proxy with a forwarding handler, though, the this value // is different in the two cases. var handler = { target: o, get: function(receiver, name) { return this.target[name]; // Same as Proxy.Handler.prototype.get } }
If you get rid of the "get" trap (you need a getOwnPropertyDescriptor trap though), the default get (derived) trap will be called instead and basically do what you do afterward (call to getOwnPropertyDescriptor trap which will call Object.getOwnPropertyDescriptor(this.target, name)+d.get.call(receiver)). The 'this' binding is performed by the engine (with the receiver as value) after the property descriptor has been returned (as explained in strawman:proxy_drop_receiver).
I've come across a similar issue (it was a set VS defineProperty issue, but basically used the same solution). One suggested solution is written down on the wiki at strawman:derived_traps_forwarding_handler
On 6/16/11 5:20 PM, David Bruant wrote:
Le 17/06/2011 01:54, David Flanagan a écrit :
// When we create a proxy with a forwarding handler, though, the this value // is different in the two cases. var handler = { target: o, get: function(receiver, name) { return this.target[name]; // Same as Proxy.Handler.prototype.get } } If you get rid of the "get" trap (you need a getOwnPropertyDescriptor trap though), the default get (derived) trap will be called instead and basically do what you do afterward (call to getOwnPropertyDescriptor trap which will call Object.getOwnPropertyDescriptor(this.target, name)+d.get.call(receiver)). The 'this' binding is performed by the engine (with the receiver as value) after the property descriptor has been returned (as explained in strawman:proxy_drop_receiver).
Thanks. I didn't realize that the default get behavior was to do what I put in my second handler.
Still, my basic question remains, though: is it a bug that a forwarding proxy created with Proxy.Handler behaves differently in this way (this value in methods is different than the this value for accessors) than the object to which it forwards?
(Originally I had a second question about the proposed removal of the receiver argument, but now I understand that that strawman is contingent on the addition of a proxy argument to the handler methods, so let's disregard that second question...)
Le 17/06/2011 08:42, David Flanagan a écrit :
On 6/16/11 5:20 PM, David Bruant wrote:
Le 17/06/2011 01:54, David Flanagan a écrit :
// When we create a proxy with a forwarding handler, though, the this value // is different in the two cases. var handler = { target: o, get: function(receiver, name) { return this.target[name]; // Same as Proxy.Handler.prototype.get } } If you get rid of the "get" trap (you need a getOwnPropertyDescriptor trap though), the default get (derived) trap will be called instead and basically do what you do afterward (call to getOwnPropertyDescriptor trap which will call Object.getOwnPropertyDescriptor(this.target, name)+d.get.call(receiver)). The 'this' binding is performed by the engine (with the receiver as value) after the property descriptor has been returned (as explained in strawman:proxy_drop_receiver).
Thanks. I didn't realize that the default get behavior was to do what I put in my second handler.
Yep. harmony:proxies#trap_defaults Besides type checking, the code is the same than yours for the get trap. In a way, it's reassuring that people experimenting like you do reach the same solution to the same problem independently.
Still, my basic question remains, though: is it a bug that a forwarding proxy created with Proxy.Handler behaves differently in this way (this value in methods is different than the this value for accessors) than the object to which it forwards?
I do not think it's a bug. What happens with your code is coherent with ECMAScript (and apparently, the JS implementation is consistent with that too). I think that a more important question is: "can you achieve what you want (giving the impression that "this" is the same for both getters/setters and methods) with the current proxy API?". If so, is it "natural" or is it a hack? Apparently, you can achieve what you want and my opinion is that the solution is natural (using the derived trap default behavior). Removing a trap to achieve your goal may sound not natural, but strawman:derived_traps_forwarding_handler seems to be a decent solution for that.
(Originally I had a second question about the proposed removal of the receiver argument, but now I understand that that strawman is contingent on the addition of a proxy argument to the handler methods, so let's disregard that second question...)
Yes, I'm sorry, I should have mentionned that. I tend to consider this strawman as already accepted since it sounds so natural to me and has found several justifications on es-discuss. It is however true it still needs to be accepted (July TC-39 meeting, hopefully?) and SpiderMonkey will need to be adjusted consequently.
I agree with all of David's points. I wonder though: would it make more sense if we'd spec. the "get" trap of Proxy.Handler as:
get: function(name, proxy) { let val = this.target[name]; return typeof val === "function" ? val.bind(this.target) : val; }
Then both method & accessor invocation bind |this| to the target.
It's hard to say what the expected default behavior is for the default forwarding handler: should it leave |this| bound to the proxy or to the target when forwarding a method invocation? As David mentioned, programmers can always implement either, but what should be the default? Creating a new bound function per |get| invocation may be expensive though. Also, treating a property bound to a function as a method is an imperfect heuristic.
Cheers, Tom
2011/6/17 David Bruant <david.bruant at labri.fr>
Le 19/06/2011 15:53, Tom Van Cutsem a écrit :
I agree with all of David's points. I wonder though: would it make more sense if we'd spec. the "get" trap of Proxy.Handler as:
get: function(name, proxy) { let val = this.target[name]; return typeof val === "function" ? val.bind(this.target) : val; }
Then both method & accessor invocation bind |this| to the target.
It's hard to say what the expected default behavior is for the default forwarding handler: should it leave |this| bound to the proxy or to the target when forwarding a method invocation? As David mentioned, programmers can always implement either, but what should be the default? Creating a new bound function per |get| invocation may be expensive though. Also, treating a property bound to a function as a method is an imperfect heuristic.
And with such a default implementation there would be an object identity problem:
var target = {}; target.a = function(){}; var f = new ForwardingProxy(target);
f.a !== f.a; // true, because a new bound function is returned each time
I suggest to not call it default, use other better name.
With ordinary objects, methods and property getter functions are both invoked on the object itself. If this object is proxied with Proxy.Handler, however, the property getter function will be invoked on the original object and the method will be invoked on the proxy object.
The code below demonstrates.
I find this surprising, though I'm not sure whether it constitutes a bug in Proxy.Handler. I do think it is at least a reason why the receiver argument to get() is necessary and should not be removed as proposed by strawman:proxy_drop_receiver
The code that demonstrates this is below. It runs in the current Firefox Aurora. I have actually been affected by this issue when the proxy object is stored in a WeakMap but the object it is forwarding to is not in the WeakMap.
if (this.console) print = console.log.bind(console);
// An object with an accessor property and a method. var o = { get property() { return map.get(this); }, method: function() { return map.get(this); } };
var map = new WeakMap(); map.set(o, "o");
// Both the getter function and method are invoked on o print(o.property); // prints "o" print(o.method()); // prints "o"
// This works for inherited properties and methods, too var p = Object.create(o); map.set(p, "p"); print(p.property); // prints "p" print(p.method()); // ditto
// When we create a proxy with a forwarding handler, though, the this value // is different in the two cases. var handler = { target: o, get: function(receiver, name) { return this.target[name]; // Same as Proxy.Handler.prototype.get } }
var q = Proxy.create(handler); map.set(q, "q"); print(q.property); // Prints "o" print(q.method()); // Prints "q"
// In order to invoke the getter function on the same object as the method // we have to go to more trouble and use code that is probably much slower. // And, we need that first receiver argument. var handler2 = { target: o, get: function(receiver, name) { var d = Object.getOwnPropertyDescriptor(this.target, name); if (d.value) return d.value; else return d.get.call(receiver); } };
var r = Proxy.create(handler2); map.set(r, "r"); print(r.property); // Prints "r" print(r.method()); // Prints "r"