Why does a JavaScript class getter for a private field fail using a Proxy?

Jordan Harband ljharb at gmail.com
Mon Jul 27 22:32:38 UTC 2020


oh oops, i meant `if (o !== this)` not `if (o.foo !== this)` :-)

On Mon, Jul 27, 2020 at 3:06 PM #!/JoePea <joe at trusktr.io> wrote:

> > `o.foo(); // works`
>
> I think there is something missing from that example as that line
> throws before it can get to the `new Proxy` line.
>
> #!/JoePea
>
> On Wed, Jul 15, 2020 at 10:47 PM Jordan Harband <ljharb at gmail.com> wrote:
> >
> > So can:
> > ```jsx
> > const o = { foo() { if (o.foo !== this) { throw 'detected'; } } };
> > o.foo(); // works
> > new Proxy(o, {}).foo(); // throws
> > ```
> >
> > (as would a class that used a closed-over WeakMap for each "private
> field")
> >
> > Private fields do not introduce any new hazards here.
> >
> > On Tue, Jul 14, 2020 at 8:18 PM #!/JoePea <joe at trusktr.io> wrote:
> >>
> >> >  private members (safely) allow classes with internal slots.
> >>
> >> I'd say that they aren't safe, if they can break 3rd-party code on the
> >> external public side.
> >>
> >> #!/JoePea
> >>
> >> On Sun, Jul 12, 2020 at 3:09 PM Michael Theriot
> >> <michael.lee.theriot at gmail.com> wrote:
> >> >
> >> > I assume OP wants to use proxies and private members together. They
> are not designed to be compatible.
> >> >
> >> > Proxies and private members are a UX goal primarily for developers.
> Proxies easily allow observation of another object or creation of exotic
> objects (e.g. Array), and private members (safely) allow classes with
> internal slots. Since they cannot be used together the issue exists, and
> the hack circumvents this by reimplementing private in a way that does not
> require private fields.
> >> >
> >> > On Sun, Jul 12, 2020 at 4:45 PM kai zhu <kaizhu256 at gmail.com> wrote:
> >> >>
> >> >> as product-developer, can i ask what ux-objective you ultimately
> want achieved?
> >> >>
> >> >> ```js
> >> >> const sub = new Sub()
> >> >>
> >> >> // i'm a noob on proxies. what is this thing (with
> proxied-private-fields) ultimately used for?
> >> >> const proxy = new Proxy(sub, ...)
> >> >> ```
> >> >>
> >> >> On Sun, Jul 12, 2020 at 4:34 PM Michael Theriot <
> michael.lee.theriot at gmail.com> wrote:
> >> >>>
> >> >>> This does require you to have both the key and the weakmap though,
> so it actually does succeed in hiding the data so long as the weakmap is
> out of scope. I guess the issue I can foresee is that the key could be
> modified after the object is created.
> >> >>>
> >> >>> e.g.
> >> >>> ```js
> >> >>> var a = new A();
> >> >>> var key = Object.getOwnPropertySymbols(a)[0];
> >> >>> delete a[key];
> >> >>> a.hidden; // throws
> >> >>> ```
> >> >>>
> >> >>> That itself can be guarded by just making the key undeletable. So,
> I guess this solution could work depending what your goals are?
> >> >>>
> >> >>> On Sun, Jul 12, 2020 at 4:21 PM Michael Theriot <
> michael.lee.theriot at gmail.com> wrote:
> >> >>>>
> >> >>>> It nearly works, but the issue is that the key will be leaked by
> `Object.getOwnPropertySymbols(new A())`, so it's not truly private.
> >> >>>>
> >> >>>> There have been ideas proposing "private symbols" but I am not
> familiar with their issues, and I would guess with Class Fields they are
> unlikely to materialize anyway.
> >> >>>>
> >> >>>> On Sun, Jul 12, 2020 at 2:19 PM François REMY <
> francois.remy.dev at outlook.com> wrote:
> >> >>>>>
> >> >>>>> At the risk of pointing out the obvious:
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> ```js
> >> >>>>>
> >> >>>>> const privkey = Symbol();
> >> >>>>>
> >> >>>>> const stores = new WeakMap();
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> class A {
> >> >>>>>
> >> >>>>>   [privkey] = {};
> >> >>>>>
> >> >>>>>   constructor() {
> >> >>>>>
> >> >>>>>     const priv = {};
> >> >>>>>
> >> >>>>>     priv.hidden = Math.random();
> >> >>>>>
> >> >>>>>     stores.set(this[privkey], priv);
> >> >>>>>
> >> >>>>>   }
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>>   get hidden() {
> >> >>>>>
> >> >>>>>     const priv = stores.get(this[privkey]);
> >> >>>>>
> >> >>>>>     return priv.hidden;
> >> >>>>>
> >> >>>>>   }
> >> >>>>>
> >> >>>>> }
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> var as = [
> >> >>>>>
> >> >>>>>                 new A(),
> >> >>>>>
> >> >>>>>                 new Proxy(new A(),{}),
> >> >>>>>
> >> >>>>>                 new Proxy(new A(),{}),
> >> >>>>>
> >> >>>>> ];
> >> >>>>>
> >> >>>>> console.log(as.map(a=>a.hidden));
> >> >>>>>
> >> >>>>> ```
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> From: Michael Theriot
> >> >>>>> Sent: Sunday, July 12, 2020 20:59
> >> >>>>> To: Michael Haufe
> >> >>>>> Cc: es-discuss at mozilla.org
> >> >>>>> Subject: Re: Why does a JavaScript class getter for a private
> field fail using a Proxy?
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> I experienced this issue prior to this proposal, using weakmaps
> for private access.
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> e.g.
> >> >>>>>
> >> >>>>> ```js
> >> >>>>>
> >> >>>>> const stores = new WeakMap();
> >> >>>>>
> >> >>>>> class A {
> >> >>>>>   constructor() {
> >> >>>>>     const priv = {};
> >> >>>>>     priv.hidden = 0;
> >> >>>>>     stores.set(this, priv);
> >> >>>>>   }
> >> >>>>>
> >> >>>>>   get hidden() {
> >> >>>>>     const priv = stores.get(this);
> >> >>>>>     return priv.hidden;
> >> >>>>>   }
> >> >>>>> }
> >> >>>>>
> >> >>>>> const a = new A();
> >> >>>>> console.log(a.hidden); // 0
> >> >>>>>
> >> >>>>> const p = new Proxy(a, {});
> >> >>>>> console.log(p.hidden); // throws!
> >> >>>>>
> >> >>>>> ```
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> I found a workaround:
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> ```js
> >> >>>>> const stores = new WeakMap();
> >> >>>>>
> >> >>>>> class A {
> >> >>>>>   constructor() {
> >> >>>>>     const priv = {};
> >> >>>>>     priv.hidden = 0;
> >> >>>>>     stores.set(this, priv);
> >> >>>>>
> >> >>>>>     const p = new Proxy(this, {});
> >> >>>>>     stores.set(p, priv); // set proxy to map to the same private
> store
> >> >>>>>
> >> >>>>>     return p;
> >> >>>>>   }
> >> >>>>>
> >> >>>>>   get hidden() {
> >> >>>>>     const priv = stores.get(this); // the original instance and
> proxy both map to the same private store now
> >> >>>>>     return priv.hidden;
> >> >>>>>   }
> >> >>>>> }
> >> >>>>>
> >> >>>>> const a = new A();
> >> >>>>>
> >> >>>>> console.log(a.hidden);
> >> >>>>> ```
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> Not ideal, and only works if you provide the proxy in the first
> place (e.g. making exotic JS objects). But, not necessarily a new issue
> with proxies, either.
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> On Fri, Jun 5, 2020 at 12:29 AM Michael Haufe <
> tno at thenewobjective.com> wrote:
> >> >>>>>
> >> >>>>> This is a known issue and very painful for me as well. You can
> see a long ugly discussion here:
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> <https://github.com/tc39/proposal-class-fields/issues/106>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> I suggest the following guide to assist you:
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> <https://javascript.info/proxy#proxy-limitations>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> Another possible approach is to have your classes extend a proxy:
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> <
> https://github.com/tc39/proposal-class-fields/issues/106#issuecomment-397484713
> >
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> From: es-discuss <es-discuss-bounces at mozilla.org> On Behalf Of
> Laurie Harper
> >> >>>>> Sent: Friday, June 5, 2020 12:21 AM
> >> >>>>> To: es-discuss at mozilla.org
> >> >>>>> Subject: Why does a JavaScript class getter for a private field
> fail using a Proxy?
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> I can expose private class fields in JavaScript using getters,
> and those getters work correctly when invoked on instances of a subclass.
> However, if I then wrap the instance with a proxy the getter will throw a
> type error, even if the proxy `get` hook uses `Reflect.get()`:
> >> >>>>>
> >> >>>>> ```
> >> >>>>> class Base {
> >> >>>>>     _attrA
> >> >>>>>     #_attrB
> >> >>>>>
> >> >>>>>     constructor() {
> >> >>>>>         this._attrA = 100
> >> >>>>>         this.#_attrB = 200
> >> >>>>>     }
> >> >>>>>
> >> >>>>>     get A() { return this._attrA }
> >> >>>>>
> >> >>>>>     get B() { return this.#_attrB }
> >> >>>>>
> >> >>>>>     incrA() { this._attrA++ }
> >> >>>>>
> >> >>>>>     incrB() { this.#_attrB++ }
> >> >>>>> }
> >> >>>>>
> >> >>>>> class Sub extends Base {}
> >> >>>>>
> >> >>>>> const sub = new Sub()
> >> >>>>>
> >> >>>>> const proxy = new Proxy(sub, {
> >> >>>>>     get(target, prop, receiver) {
> >> >>>>>         const value = Reflect.get(target, prop, receiver)
> >> >>>>>         return typeof value === 'function' ? value.bind(target) :
> value // (1)
> >> >>>>>     }
> >> >>>>> })
> >> >>>>>
> >> >>>>> console.log('sub.A', sub.A) // OK: -> 100
> >> >>>>> console.log('sub.B', sub.B) // OK: -> 200
> >> >>>>> sub.incrA() // OK
> >> >>>>> sub.incrB() // OK
> >> >>>>> console.log('sub.A', sub.A) // OK: -> 101
> >> >>>>> console.log('sub.B', sub.B) // OK: -> 201
> >> >>>>>
> >> >>>>> console.log('proxy.A', proxy.A) // OK: -> 100
> >> >>>>> console.log('proxy.B', proxy.B) // TypeError: Cannot read private
> member #_attrB from an object whose class did not declare it
> >> >>>>> proxy.incrA() // OK
> >> >>>>> proxy.incrB() // OK due to (1)
> >> >>>>> console.log('proxy.A', proxy.A) // OK: -> 100
> >> >>>>> console.log('proxy.B', proxy.B) // TypeError: Cannot read private
> member #_attrB from an object whose class did not declare it
> >> >>>>> ```
> >> >>>>>
> >> >>>>> The call to `proxy.incrB()` works, because the proxy handler
> explicitly binds function values to `target` on line (1). Without the
> `bind()` call, the `proxy.incrB()` invocation would throw a `TypeError`
> like the getter invocation does. That makes some sense: the result of the
> call to `Reflect.get()` is the 'unbound' function value of the property
> being retrieved, which must then be bound to `target`; it would make more
> sense, though, if `this` binding was applied by the [[Call]] operation on
> the result of the [[Get]] operation...
> >> >>>>>
> >> >>>>> But there is no opportunity to 'bind' a getter before invoking
> it; as a result, a proxied getter ends up receiving the wrong `this`
> binding, leading to the inconsistency.
> >> >>>>>
> >> >>>>> Is there any way to make this work correctly? The only approach I
> can think of (which I haven't tried) would be to have the `get` hook walk
> up the prototype chain, starting from `target`, calling
> `getOwnPropertyDescriptor()` and checking for a getter method, and
> explicitly applying the getter with an adjusted `this` binding. That sounds
> ludicrously cumbersome and brittle...
> >> >>>>>
> >> >>>>> Is there a better way to get this working correctly?
> >> >>>>>
> >> >>>>>
> >> >>>>>
> >> >>>>> --
> >> >>>>>
> >> >>>>> Laurie
> >> >>>>>
> >> >>>>> _______________________________________________
> >> >>>>> 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
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20200727/d782cbe0/attachment-0001.html>


More information about the es-discuss mailing list