Proxy handler.has() does not have a receiver argument?

Tom Van Cutsem tomvc.be at gmail.com
Fri Mar 18 22:43:55 UTC 2016


I was on board with potentially making `has()` receiver-dependent but you
lost me when you expected `getOwnPropertyNames` to trigger a trap on a
proxy-as-prototype. Adding `receiver` to every MOP method is a no-go. It
fundamentally changes the meaning of these operations and it would destroy
the pay-only-when-you-use-it performance model of proxies, since operations
that used to be local to only the 'own' object would now need to search the
proto-chain for a potential proxy trap.

Using a proxy-as-prototype was never intended as a way to be able to
intercept arbitrary operations on whatever object happened to inherit from
the proxy. A proxy-as-prototype still emulates only a single object. It
just happens to be an object that serves as a prototype for other objects.

Cheers,
Tom

2016-03-18 23:18 GMT+01:00 Michael Theriot <michael.lee.theriot at gmail.com>:

> I think I figured out how to make inheritance work...
>
> ```js
> var wm1 = new WeakMap(); function A() { let proxy = new Proxy(this, { get:
> (target, property, receiver) => property === 'bacon' || target[property]
> }); wm1.set(proxy, {}); return proxy; } var wm2 = new WeakMap(); function
> B() { let proxy = A.call(new Proxy(this, { get: (target, property,
> receiver) => property === 'ham' || target[property] })); wm2.set(proxy,
> {}); return proxy; } var a = new A(); var b = new B(); wm1.has(a); // true
> wm2.has(a); // false wm1.has(b); // true wm2.has(b); // true
>
> a.bacon; // true
> a.ham; // undefined
>
> b.bacon; // true
> b.ham; // true
> ```
>
> I can't imagine this is good for optimizations though. But I guess it does
> what I need.
>
> On Fri, Mar 18, 2016 at 4:45 PM, Michael Theriot <
> michael.lee.theriot at gmail.com> wrote:
>
>> Michael’s preferred approach also introduces observable irregularity into
>>> the standard JS inheritance model for ordinary objects.
>>> Consider an object created using Michael’s preferred approach:
>>> ```js
>>> var arr = [0, 1];
>>> console.log(Reflect.has(arr,”0”));  //true, arr has “0” as an own
>>> property
>>> var subArr = Object.create(arr);
>>> console.log(Reflect.has(subArr,”0”));  //true, all unshadowed properties
>>> of proto visible from ordinary objects
>>> var b = new ArrayView2(arr);
>>> console.log(Reflect.has(b,”0”));  //true, prototype proxy makes array
>>> elements visible as if properties of b
>>> var subB= Object.create(b);
>>> console.log(Reflect.has(subB,”0”));  //false, some unshadowed properties
>>> of proto is not visible from subB
>>> ```
>>
>>
>> I think this relates to the original concern; if you could pass the
>> receiver this could be resolved. That still leaves `getOwnPropertyNames`
>> reporting wrong values though and I see no foreseeable way to resolve that
>> without returning an actual proxy itself.
>>
>> The reason I'm trying this approach is because I read on the MDN that
>> when used in the prototype a `receiver` argument is passed that references
>> the instance, so I assumed this was the intent behind it. The only other
>> explanation I could think of is that proxies have a receiver to mimic the
>> `Reflect.set`/`Reflect.get` methods which need a receiver for
>> getters/setters to work properly, not so you can use them on the prototype
>> chain.
>>
>> The other case I would make is every instance would have an identical
>> proxy, and it just makes sense to put that on the prototype for the same
>> reasons you put shared methods/properties there.
>>
>> Note that we are not really talking about a new capability here.
>>> Michael’s first design shows that ES proxies already have the capability to
>>> implement the object level semantics he desires.
>>
>>
>> To be fair I had several obstacles with inheritance using the first
>> version.
>>
>> ```js
>> var wm1 = new WeakMap();
>>
>> function A() {
>>   wm1.set(this, {});
>>   return new Proxy(this, {});
>> }
>>
>> var wm2 = new WeakMap();
>>
>> function B() {
>>   A.call(this);
>>   wm2.set(this, {});
>>   return new Proxy(this, {});
>> }
>>
>> var a = new A();
>> var b = new B();
>>
>> wm1.has(a); // true
>> wm2.has(a); // false
>>
>> wm1.has(b); // false
>> wm2.has(b); // true
>> ```
>>
>> As you can see storing a reference to `this` can't work anymore, since we
>> actually return a proxy. You can try to work around this...
>>
>> ```js
>> var wm1 = new WeakMap();
>>
>> function A() {
>>   let self = this;
>>   if(new.target === A) {
>>     self = new Proxy(this, {});
>>   }
>>   wm1.set(self, {});
>>   return self;
>> }
>>
>> var wm2 = new WeakMap();
>>
>> function B() {
>>   let self = this;
>>   if(new.target === B) {
>>     self = new Proxy(this, {});
>>   }
>>   A.call(self);
>>   wm2.set(self, {});
>>   return self;
>> }
>>
>> var a = new A();
>> var b = new B();
>>
>> wm1.has(a); // true
>> wm2.has(a); // false
>>
>> wm1.has(b); // true
>> wm2.has(b); // true
>> ```
>>
>> But then problems arise because the new proxy doesn't go through the old
>> proxy. So anything guaranteed by A()'s proxy is not guaranteed by B()'s
>> proxy.
>>
>> ```js
>> var wm1 = new WeakMap();
>>
>> function A() {
>>   let self = this;
>>   if(new.target === A) {
>>     self = new Proxy(this, {
>>       get: (target, property, receiver) => property === 'bacon' ||
>> target[property]
>>     });
>>   }
>>   wm1.set(self, {});
>>   return self;
>> }
>>
>> var wm2 = new WeakMap();
>>
>> function B() {
>>   let self = this;
>>   if(new.target === B) {
>>     self = new Proxy(this, {
>>       get: (target, property, receiver) => property === 'ham' ||
>> target[property]
>>     });
>>   }
>>   A.call(self);
>>   wm2.set(self, {});
>>   return self;
>> }
>>
>> var a = new A();
>> var b = new B();
>>
>> wm1.has(a); // true
>> wm2.has(a); // false
>>
>> wm1.has(b); // true
>> wm2.has(b); // true
>>
>> a.bacon; // true
>> a.ham; // undefined
>>
>> b.bacon; // undefined
>> b.ham; // true
>> ```
>>
>> (I'm open to solutions on this particular case... One that doesn't
>> require me to leak the handler of the A proxy)
>>
>> Ultimately I can actually achieve both what I want with the ArrayView
>> example and inheritance by using a **lot** of `defineProperty` calls on
>> `this` in the constructor, but performance is a disaster as you might
>> expect.
>>
>> On Fri, Mar 18, 2016 at 2:55 PM, Andrea Giammarchi <
>> andrea.giammarchi at gmail.com> wrote:
>>
>>> AFAIK the reason there is a `receiver` is to deal with prototype cases
>>> ... if that was a good enough reason to have one, every prototype case
>>> should be considered for consistency sake.
>>>
>>> We've been advocating prototypal inheritance for 20 years and now it's
>>> an obstacle or "not how JS is"?
>>>
>>> ```js
>>> class Magic extends new Proxy(unbe, lievable) {
>>>   // please make it happen
>>>   // as it is now, that won't work at all
>>> }
>>> ```
>>>
>>> Best Regards
>>>
>>>
>>> On Fri, Mar 18, 2016 at 7:30 PM, Mark S. Miller <erights at google.com>
>>> wrote:
>>>
>>>> I agree with Allen. I am certainly willing -- often eager -- to revisit
>>>> and revise old design decisions that are considered done, when I think the
>>>> cost of leaving it alone exceeds the cost of changing it. In this case, the
>>>> arguments that this extra parameter would be an improvement seem weak. Even
>>>> without the revising-old-decision costs, I am uncertain which decision I
>>>> would prefer. Given these costs, it seems clear we should leave this one
>>>> alone.
>>>>
>>>> Unless it turns out that the cost of leaving it alone is much greater
>>>> than I have understood. If so, please help me see what I'm missing.
>>>>
>>>>
>>>>
>>>>
>>>>
>>>>
>>>> On Fri, Mar 18, 2016 at 12:17 PM, Allen Wirfs-Brock <
>>>> allen at wirfs-brock.com> wrote:
>>>>
>>>>>
>>>>> On Mar 18, 2016, at 9:24 AM, Andrea Giammarchi <
>>>>> andrea.giammarchi at gmail.com> wrote:
>>>>>
>>>>> Agreed with everybody else the `receiver` is always needed and `Proxy`
>>>>> on the prototype makes way more sense than per instance.
>>>>>
>>>>>
>>>>> I don’t agree.  While you certainly can imagine a language where each
>>>>> object’s “prototype” determines that object’s fundamental behaviors and
>>>>> provides the MOP intercession hooks(in fact that’s how most class-based
>>>>> languages work).  But that’s not the JS object model.  Each JS object is
>>>>> essentially a singleton that defines it’s own fundamental behaviors.
>>>>> Whether or this model is better or worse than the class-based model isn't
>>>>> really relevant, but in the context of JS there are advantage to
>>>>> consistently adhering to that model,
>>>>>
>>>>> For example, in Michael’s  desired approach, the instance objects of
>>>>> his ArrayView abstraction are “ordinary objects”.  One of the fundamental
>>>>> behavioral characteristics of ordinary objects is that all of there own
>>>>> properties are defined and available to the implementation in a standard
>>>>> way. Implementations certainly make use of that characteristic for
>>>>> optimization purposes. Michael’s approach would make such optimizations
>>>>> invalid because every time an own property needed to be access a prototype
>>>>> walk would have to be performed  because there might be an exotic object
>>>>> somewhere on the prototype chain that was injecting own property into the
>>>>> original “receiver”.
>>>>>
>>>>> Michael’s preferred approach also introduces observable irregularity
>>>>> into the standard JS inheritance model for ordinary objects.
>>>>>
>>>>> Consider an object created using Michael’s preferred approach:
>>>>>
>>>>> ```js
>>>>> var arr = [0, 1];
>>>>> console.log(Reflect.has(arr,”0”));  //true, arr has “0” as an own
>>>>> property
>>>>> var subArr = Object.create(arr);
>>>>> console.log(Reflect.has(subArr,”0”));  //true, all unshadowed
>>>>> properties of proto visible from ordinary objects
>>>>>
>>>>> var b = new ArrayView2(arr);
>>>>> console.log(Reflect.has(b,”0”));  //true, prototype proxy makes array
>>>>> elements visible as if properties of b
>>>>> var subB= Object.create(b);
>>>>> console.log(Reflect.has(subB,”0”));  //false, some unshadowed
>>>>> properties of proto is not visible from subB
>>>>> ```
>>>>>
>>>>> Note the his original Proxy implementation does not have this
>>>>> undesirable characteristic.
>>>>>
>>>>> So what about the use of `receiver` in [[Get]]/[[Set]].  That’s a
>>>>> different situation.  [[Get]]/[[Set]] are not fundamental, rather they are
>>>>> derived (they work by applying other more fundamental MOP operations). The
>>>>> `receiver` argument is not used by them to perform property lookup (they
>>>>> use [[GetOwnProperty]] and [[GetPrototypeOf]]) for the actual property
>>>>> lookup).  `receiver` is only used in the semantics of what happens after
>>>>> the property lookup occurs.  Adding a `receiver` argument to the other MOP
>>>>> operations for the purpose of changing property lookup semantics seems like
>>>>> a step too far. The ES MOP design is a balancing act between capability,
>>>>> implementability, and consistency. I think adding `receiver` to every MOP
>>>>> operation would throw the design out of balance.
>>>>>
>>>>> Finally,
>>>>>
>>>>> Note that we are not really talking about a new capability here.
>>>>> Michael’s first design shows that ES proxies already have the capability to
>>>>> implement the object level semantics he desires. So, we are only talking
>>>>> about exactly how he goes about using Proxy to implement that semantics. He
>>>>> would prefer a different Proxy design than what was actually provided by
>>>>> ES6. But that isn’t what was specified or what has now been implemented. We
>>>>> can all imagine how many JS features might be “better” if they worked
>>>>> somewhat differently. But that generally isn’t an option. The existing
>>>>> language features and their implementations are what they are and as JS
>>>>> programmers we need to work within that reality.
>>>>>
>>>>> Allen
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>> Also the `getPrototypeOf` trap is really pointless right now
>>>>>
>>>>> ```js
>>>>> function Yak() {}
>>>>> Yak.prototype = new Proxy(Yak.prototype, {
>>>>>   getPrototypeOf: (target) => console.log('lulz')
>>>>> });
>>>>>
>>>>> var yup = new Yak;
>>>>> Object.getPrototypeOf(yup);
>>>>> ```
>>>>>
>>>>> The `target` is actually the original `Yak.prototype` which is already
>>>>> the `yup` prototype: useless trap if used in such way.
>>>>>
>>>>> Being also unable to distinguish between `getOwnPropertyNames` vs
>>>>> `keys` is a bit weird.
>>>>>
>>>>> `Proxy` looks so close to be that powerful but these bits make it
>>>>> kinda useless for most real-world cases I've been recently dealing with.
>>>>>
>>>>> Thanks for any sort of improvement.
>>>>>
>>>>> Regards
>>>>>
>>>>>
>>>>>
>>>>>
>>>>> On Fri, Mar 18, 2016 at 1:54 PM, Michael Theriot <
>>>>> michael.lee.theriot at gmail.com> wrote:
>>>>>
>>>>>> I'm trying to make the proxy-as-a-prototype pattern work but I've
>>>>>> just discovered the `ownKeys` trap is never called on traps on the
>>>>>> prototype. So even if the `has` trap is allowed to see the `receiver`, and
>>>>>> thus verify the properties "0", "1" exist, this pattern would fail to
>>>>>> return the properties "0", "1" exist on an `Object.getOwnPropertyNames`
>>>>>> call. Disappointing! I'd rather use a proxy on the prototype than create
>>>>>> one for each instance but without a correct `ownKeys` return it just
>>>>>> doesn't come full circle. Is there a trick to make this work or am I out of
>>>>>> luck here? I can only think of actually defining the properties to make it
>>>>>> work, which defeats the idea of using a proxy on the prototype to begin
>>>>>> with.
>>>>>>
>>>>>> Regardless I agree that traps called on a prototype chain should
>>>>>> always receive the `receiver` as an argument. I think the only trap other
>>>>>> than `set`, `get`, and `has` that can do this is the `getPrototypeOf` trap
>>>>>> (currently does not have a `receiver`) when the `instanceof` check needs to
>>>>>> climb the prototype chain.
>>>>>>
>>>>>> On Thu, Mar 17, 2016 at 6:29 PM, Tom Van Cutsem <tomvc.be at gmail.com>
>>>>>> wrote:
>>>>>>
>>>>>>> The rationale for not having a `receiver` argument to `has` is that
>>>>>>> the value produced by the "in" operator is not normally dependent on the
>>>>>>> receiver. This is in contrast with `get` and `set` which may find an
>>>>>>> accessor up the proto chain that needs to run with a `this` bound to the
>>>>>>> receiver.
>>>>>>>
>>>>>>> That said, I follow your line of reasoning and it is true that
>>>>>>> `has`, `get` and `set` are the three traps that can be called on a
>>>>>>> proxy-used-as-prototype (now that `enumerate` is considered deprecated), so
>>>>>>> it would be consistent to allow all of them to  refer back to the original
>>>>>>> receiver. This enables the general pattern that you illustrate.
>>>>>>>
>>>>>>> As you note, the weirdness of this is apparent because it doesn't
>>>>>>> normally make sense to pass a `receiver` argument to Reflect.has().
>>>>>>> However, if `receiver` would be made visible in a Proxy handler's `has`
>>>>>>> trap, then `Reflect.has` should nevertheless be likewise extended so that
>>>>>>> one can faithfully forward the `receiver` argument.
>>>>>>>
>>>>>>> Spec-wise, I think the only significant change is that 7.3.10
>>>>>>> HasProperty
>>>>>>> <http://www.ecma-international.org/ecma-262/6.0/#sec-hasproperty>,
>>>>>>> step 3 must be changed to `O.[[HasProperty]](P, O)` and all [[HasProperty]]
>>>>>>> internal methods must likewise be extended with an extra argument (which
>>>>>>> they ignore). Only the Proxy implementation in 9.5.7 would then actually
>>>>>>> refer to that argument.
>>>>>>>
>>>>>>> Cheers,
>>>>>>> Tom
>>>>>>>
>>>>>>> 2016-03-17 11:46 GMT+01:00 Michael Theriot <
>>>>>>> michael.lee.theriot at gmail.com>:
>>>>>>>
>>>>>>>> I feel like it should, or I am misunderstanding something
>>>>>>>> fundamental. I made a basic scenario to explain:
>>>>>>>>
>>>>>>>> ```js
>>>>>>>> var arrays = new WeakMap();
>>>>>>>>
>>>>>>>> function ArrayView(array) {
>>>>>>>>   arrays.set(this, array);
>>>>>>>>
>>>>>>>>   return new Proxy(this, {
>>>>>>>>     set: (target, property, value) => (arrays.has(this) && property
>>>>>>>> in arrays.get(this))  ? arrays.get(this)[property] = value :
>>>>>>>> target[property] = value,
>>>>>>>>     get: (target, property)        => (arrays.has(this) && property
>>>>>>>> in arrays.get(this))  ? arrays.get(this)[property]         :
>>>>>>>> target[property],
>>>>>>>>     has: (target, property)        => (arrays.has(this) && property
>>>>>>>> in arrays.get(this)) || property in target
>>>>>>>>   });
>>>>>>>> }
>>>>>>>>
>>>>>>>> ArrayView.prototype = Object.create(Array.prototype, {
>>>>>>>>   arrayLength: {
>>>>>>>>     get() {
>>>>>>>>       return arrays.get(this).length;
>>>>>>>>     }
>>>>>>>>   }
>>>>>>>> });
>>>>>>>> ```
>>>>>>>>
>>>>>>>> When `new ArrayView(somearray)` is called the reference to
>>>>>>>> `somearray` is stored in the `arrays` weak map and a proxy is returned that
>>>>>>>> allows you to manipulate indices on it, or fallback to the object for other
>>>>>>>> properties.
>>>>>>>>
>>>>>>>> This could be simplified by putting the proxy on the prototype
>>>>>>>> chain to reduce overhead and actually return a genuine `ArrayView` object
>>>>>>>> instead:
>>>>>>>>
>>>>>>>> ```js
>>>>>>>> var arrays = new WeakMap();
>>>>>>>>
>>>>>>>> function ArrayView2(array) {
>>>>>>>>   arrays.set(this, array);
>>>>>>>> }
>>>>>>>>
>>>>>>>> var protoLayer = Object.create(Array.prototype, {
>>>>>>>>   arrayLength: {
>>>>>>>>     get() {
>>>>>>>>       return arrays.get(this).length;
>>>>>>>>     }
>>>>>>>>   }
>>>>>>>> });
>>>>>>>>
>>>>>>>> ArrayView2.prototype = new Proxy(protoLayer, {
>>>>>>>>   set: (target, property, value, receiver) => (arrays.has(receiver)
>>>>>>>> && property in arrays.get(receiver))  ? arrays.get(receiver)[property] =
>>>>>>>> value : Reflect.set(target, property, value, receiver),
>>>>>>>>   get: (target, property, receiver)        => (arrays.has(receiver)
>>>>>>>> && property in arrays.get(receiver))  ? arrays.get(receiver)[property]
>>>>>>>>     : Reflect.get(target, property, receiver),
>>>>>>>>   has: (target, property)                  => (arrays.has(target)
>>>>>>>> && property in arrays.get(target))   || Reflect.has(target, property)
>>>>>>>> });
>>>>>>>> ```
>>>>>>>>
>>>>>>>> Under this setup `target` refers to the protoLayer object which is
>>>>>>>> useless here, but we can use the `receiver` argument in its place to access
>>>>>>>> the weak map, and replace our set/get operations with
>>>>>>>> Reflect.set/Reflect.get calls to the target (protoLayer) using a receiver
>>>>>>>> (the instance) to pass the correct `this` value to the `arrayLength` getter
>>>>>>>> and prevent infinite recursion.
>>>>>>>>
>>>>>>>> One problem - handler.has() lacks a receiver argument. So in this
>>>>>>>> scenario when using the `in` operator it will always fail on array
>>>>>>>> properties because we cannot check the weak map by passing in the instance.
>>>>>>>>
>>>>>>>> ```js
>>>>>>>> var arr = [0, 1];
>>>>>>>>
>>>>>>>> var a = new ArrayView(arr);
>>>>>>>> a.arrayLength; // 2
>>>>>>>> 'arrayLength' in a; // true
>>>>>>>> '0' in a; // true
>>>>>>>> '1' in a; // true
>>>>>>>> '2' in a; // false
>>>>>>>>
>>>>>>>> var b = new ArrayView2(arr);
>>>>>>>> b.arrayLength; // 2
>>>>>>>> 'arrayLength' in b; // true
>>>>>>>> '0' in b; // false
>>>>>>>> '1' in b; // false
>>>>>>>> '2' in b; // false
>>>>>>>> ```
>>>>>>>>
>>>>>>>> Without a receiver argument on handler.has(), it is practically
>>>>>>>> useless for proxies used as a prototype. You can't reference the instance
>>>>>>>> calling it and your target is simply the parent prototype.
>>>>>>>>
>>>>>>>> Is there a reason the handler.has() trap should not obtain the
>>>>>>>> receiver when used on the prototype chain? I can understand why
>>>>>>>> Reflect.has() wouldn't have a receiver argument (that wouldn't make sense)
>>>>>>>> but this seems like a legitimate use for it. Otherwise I don't see a reason
>>>>>>>> to use the handler.has() trap at all on prototype proxies except for
>>>>>>>> bizarre behaviors that have nothing to do with the instance. It will always
>>>>>>>> have the same behavior across all instances since you can't differentiate
>>>>>>>> them.
>>>>>>>>
>>>>>>>> _______________________________________________
>>>>>>>> es-discuss mailing list
>>>>>>>> es-discuss at mozilla.org
>>>>>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>>
>>>>>> _______________________________________________
>>>>>> es-discuss mailing list
>>>>>> es-discuss at mozilla.org
>>>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>>>
>>>>>>
>>>>> _______________________________________________
>>>>> es-discuss mailing list
>>>>> es-discuss at mozilla.org
>>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>>
>>>>>
>>>>>
>>>>> _______________________________________________
>>>>> es-discuss mailing list
>>>>> es-discuss at mozilla.org
>>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>>
>>>>>
>>>>
>>>>
>>>> --
>>>>     Cheers,
>>>>     --MarkM
>>>>
>>>
>>>
>>> _______________________________________________
>>> es-discuss mailing list
>>> es-discuss at mozilla.org
>>> https://mail.mozilla.org/listinfo/es-discuss
>>>
>>>
>>
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160318/70e07cd1/attachment-0001.html>


More information about the es-discuss mailing list