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

Michael Theriot michael.lee.theriot at gmail.com
Sun Jul 12 21:21:35 UTC 2020


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 <michael.lee.theriot at gmail.com>
> *Sent: *Sunday, July 12, 2020 20:59
> *To: *Michael Haufe <tno at thenewobjective.com>
> *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
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20200712/faae40e7/attachment-0001.html>


More information about the es-discuss mailing list