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

Michael Theriot michael.lee.theriot at gmail.com
Fri Mar 18 22:18:48 UTC 2016


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
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160318/99184ad3/attachment-0001.html>


More information about the es-discuss mailing list