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 22:09:35 UTC 2020


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 <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
>>>>
>>>>
>>>>
>>> _______________________________________________
>> 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/48508642/attachment.html>


More information about the es-discuss mailing list