2011/10/18 David Herman <dher...@mozilla.com> > Hi Tom, this looks very promising. Some comments below; quoting the wiki > page inline. > > * target is the object which the direct proxy wraps > > > Just checking: presumably this proposal doesn't allow for target to be a > primitive, right? (Other than the special case of null you mention later.) > I.e., this is still just a spec for virtualized objects, not for virtualized > primitives. >
Correct. > > Unlike in the original proxy proposal, any non-configurable properties > reported by the handler of a direct proxy are also stored on the target > object. In doing so, the proxy ensures that the target object always > “matches” the proxy as far as non-configurability of properties is > concerned. > > > I'm confused about this. Do you mean that, if the proxy provides a property > descriptor that said .foo is non-configurable, the semantics automatically > adds the property to the target and from that point on, all operations > involving .foo go through the target instead of through the handler? Does > this let you faithfully virtualize the .length property of arrays? How? You > still want to intercept gets and sets, but you want getOwnPropertyDescriptor > to say that it's a data property, not an accessor property. > If a handler says a property "foo" is non-configurable, "foo" will indeed be automatically added to the target (think of the proxy as trying to keep the target and the virtual object described by the handler "in sync"). However, all further operations involving "foo" _still_ trap the handler. .length on arrays can be fully intercepted, but a proxy can't violate the non-configurability constraints on it (which it shouldn't want to anyway). An example: var a = []; var arrayProxy = Proxy.for(a, { get: function(name, target, proxy) { console.log("got: "+name); return target[name]; }, getOwnPropertyDescriptor: function(name, target) { if (name === "length") { return { value: 22, writable: true, configurable: false }; } return Object.getOwnPropertyDescriptor(target, name); } }); arrayProxy.length // got: "length" Object.getOwnPropertyDescriptor(arrayProxy, "length") // returns {value:22,writable:true,configurable:false,enumerable:false} // also calls Object.defineProperty(a, "length", {value:22,writable:true,configurable:false}) // which should succeed a.length // 22 (I tried to briefly test this in tracemonkey and v8, but it seems support for updating "length" is very shaky: v8: > var a = [] > Object.defineProperty(a,"length",{value:22}) > a.length 0 tracemonkey: > var a = [] > Object.defineProperty(a,"length",{value:22}) typein:2: InternalError: defining the length property on an array is not currently supported so don't expect the above example to work just yet) > > When a direct proxy is made non-extensible, so is its target. Once a direct > proxy is non-extensible, all properties reported by the handler are stored > on the target, regardless of their configurability. This ensures that the > handler cannot report any “new” properties, since the target object will now > reject any attempt to add new properties. > > > I'm still confused about when operations go through the handler and when > they go through the target. If they can still go through the handler after > making the proxy non-extensible, then what's to stop the handler from making > it look like new properties are appearing? > The rule is simple: as long as stopTrapping was not successfully invoked on a proxy, the proxy _always_ keeps trapping, regardless of extensibility of the target. However, a direct proxy will inspect the result of all handler traps and will try to keep the target "in sync" with the handler. So, if the proxy (and thus its target) is made non-extensible, and the handler subsequently reports a new property, the proxy will try to add that property to the target, which will fail, because the target is now non-extensible. This is why the proxy cannot violate the non-configurable/non-extensible invariants of the target. > > A direct proxy may acquire some of its internal properties from its target > object. This includes the [[Class]] and [[Prototype]] internal properties: > > > This is awesome. > > * typeof aProxy is equal to typeof target. > > > To make sure I'm following along correctly: the typeof result can only be > "object" or "function", right? > Indeed. Although if there would exist a host object h that according to ES5 11.4.3. returns something else, there is room in the direct proxies spec to have typeof Proxy.for(h,{}) also return that custom string. > > We could even allow for direct proxies to acquire non-standard > internal properties from their target object. This could be a useful > principle when wrapping host objects. > > > This seems important in order to make host methods work, e.g., the ones > that access the [[Value]] property. I guess you could code around it by > proxying those methods as well? > What's the [[Value]] property? I'm not sure I understand. > > For Direct Proxies, rather than adopting > the handler_access_to_proxy strawman that adds the proxy itself as an > additional argument to all traps, we propose to instead add the target as an > additional, last, argument to all traps. That allows the handler to interact > with the target that it implicitly wraps. > > > You still might want access to the identity of the proxy itself, e.g., to > store the object in a WeakMap. But as you guys have pointed out, you can > store this in the handler object, which can still inherit its traps from > prototype methods. So I guess this isn't critical. > Also, in those cases where you can assume a 1-to-1 mapping between a proxy and its handler, you might as well use the identity of the handler. Or, presumably, what would make even more sense in general is to use the identity of the target, not the proxy. > > The protect trap no longer needs to return a property descriptor map... > > > This seems like a big deal to me. The property descriptor map could > potentially be quite large. > Absolutely. > > Proxy.stopTrapping() > > > This one makes me a little queasy. I'm sure you guys already thought of and > dismissed the possibility of having Proxy.for(...) return a pair of a proxy > and a stopTrapping() thunk that's tied to the one proxy. That's obviously > got wretched ergonomics. But I'm not crazy about the idea of drive-by > deproxification. Just my initial reaction, anyway. > I agree 200%. To me, a much better interface for stopTrapping would be: var {proxy, stopTrapping} = Proxy.temporaryFor(target, handler); // ... use the proxy stopTrapping(); // switch off the proxy unconditionally, no need to ask its handler first That would remove the need for a 'stopTrapping' trap (at the expense of an arcane "temporaryFor" proxy-creation-function). IMHO removing 'stopTrapping' from the public API of a handler is a good thing. It better isolates the feature to the few experts that would need it. We didn't propose it because we suspected pushback against this type of API. In any case, if we should stick to a trap, by default that trap should _reject_ the request. Obviously, the reverse would allow arbitrary clients to switch off proxies, which would be a huge security hole. > > Both the call and new trap are optional and default to forwarding the > operation to the wrapped target function. > > > Nicely done! Much cleaner than Proxy.create and Proxy.createFunction. > > Proxy.startTrapping() (a.k.a. Proxy.attach) > > > I don't fully understand how this one works. Is it essentially a Smalltalk > become, in the sense that the existing object is turned into a proxy, and > its guts (aka brain) now become a different object that the proxy uses as > its target? > That is exactly right, although I think Smalltalk's "become" is more powerful. In the Proxy.startTrapping case, all pointers to target now become pointers to a _fresh_ proxy object. That's important implementation-wise. BTW, I added some figures to the strawman page that should help clarify things. > > So, this has obvious appeal; for example, it addresses the data binding use > cases. > That is indeed the main use case. > > But I have some serious reservations about it. For one, tying the notion of > "becomeability" to extensibility seems sub-optimal. I'm not sure you always > want an object to be non-extensible when you want it to be non-becomeable. > And a serious practical issue is whether host objects could be becomeable. > I'm pretty sure that's going to be a serious problem for implementations. > I agree in principle that "attachability" or "becomeability" is distinct from extensibility. But from a usability POV, what's the alternative? To introduce yet another bit, and to put the onus on defensive objects by requiring them to do Object.freeze(Object.cantTrap(myObject))? To me, that seems worse than tying non-extensibility to non-becomeability. You're right that startTrapping + host objects = potential trouble. My answer there would be that host objects, like non-extensible objects, are allowed to reject the request. > > It’s still as easy to create such “virtual” proxies: just pass a fresh > empty object (or perhaps even null?) > > > Please, make it null. So much more pleasant (and avoids needless > allocation). > > (The only downside of allowing null to mean "no target" would be if you > wanted to future-proof for virtualizable primitives, including a > virtualizable null.) > Avoiding needless allocation is why I proposed null, indeed. But there are some unresolved issues here: if the target is null, how should the proxy determine its typeof, [[Class]] and [[Prototype]]? This needs more thought. > > Proxy.create = function(handler, proto) { > return Proxy.for(Object.create(proto), handler); > }; > Proxy.createFunction = function(handler, call, opt_construct) { > var extHandler = Object.create(handler); > extHandler.call = call; > extHandler.new = opt_construct; > return Proxy.for(call, extHandler); > }; > > > Doesn't this leak the property table of the call function? ISTM you want: > Yes and no. There's a little note under that implementation stating that the above is only an accurate emulation of Proxy.createFunction if the handler implements all traps. For instance, the ForwardingHandler implements all traps, and then calling the above Proxy.createFunction as follows will not leak properties of the call trap as properties of the target: Proxy.createFunction(new ForwardingHandler(targetFunction), targetFunction); IMHO, if we would buy into direct proxies, I see no need to continue supporting Proxy.create{Function}. > > Proxy.createFunction = function(handler, call, opt_construct) { > var extHandler = Object.create(handler); > extHandler.call = call; > extHandler.new = opt_construct; > var target = function(...args) { return call(...args); }; // new > object > return Proxy.for(target, extHandler); > } > > We propose to bind the singleton forwarding handler to Proxy.forward. > > > Singleton within a single window (aka loader), or singleton shared across > all windows (aka loaders)? I would expect the former. > The former seems fine. I used the word "singleton" only to stress that Proxy.forward can be stateless. There's no need to generate new instances of it as was the case with the ForwardingHandler. > > Advantages w.r.t. existing proxies > > > These are pretty awesome. :) > > * Proxy.for could be renamed to Proxy.create. > > > If we were not in a module setting, I would say just make Proxy a function > (which behaves the same whether called via |new| or not), now that there's > only one function needed. But we still need to figure out our naming > conventions for the module-ized standard library, so maybe this is best > decided after we have made headway on hat. > Good idea. > * We may still want to make all traps mandatory rather than defaulting to > forwarding to the target: > > > Please, let's keep them optional. The future-proofing is going to be > crucial, I'd wager. And it will make an enormous difference in ergonomics > and palatability. The difference between saying here's a simple proxy: > > Proxy.for(obj, { get: function(key) { alert("getting " + key + "!") } > }) > > and the current state of affairs is pretty huge. > I agree. The distinction between fundamental vs. derived traps and the advantages that this brings (only having to implement the fundamental traps) can easily be offloaded into a little userland library that facilitates working with Proxies. Thanks for the feedback, Tom
_______________________________________________ es-discuss mailing list es-discuss@mozilla.org https://mail.mozilla.org/listinfo/es-discuss