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

# Laurie Harper (6 months ago)

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?

# Michael Haufe (6 months ago)

This is a known issue and very painful for me as well. You can see a long ugly discussion here:

tc39/proposal-class-fields#106

I suggest the following guide to assist you:

javascript.info/proxy#proxy-limitations

Another possible approach is to have your classes extend a proxy:

tc39/proposal-class-fields#106

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

# Michael Theriot (4 months ago)

I experienced this issue prior to this proposal, using weakmaps for private access.

e.g.

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:

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.

# François REMY (4 months ago)

At the risk of pointing out the obvious:

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<mailto:michael.lee.theriot at gmail.com>

Sent: Sunday, July 12, 2020 20:59 To: Michael Haufe<mailto:tno at thenewobjective.com>

Cc: es-discuss at mozilla.org<mailto: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.

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:

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<mailto: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:

tc39/proposal-class-fields#106

I suggest the following guide to assist you:

javascript.info/proxy#proxy-limitations

Another possible approach is to have your classes extend a proxy:

tc39/proposal-class-fields#106

From: es-discuss <es-discuss-bounces at mozilla.org<mailto: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<mailto: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

# Michael Theriot (4 months ago)

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.

# Michael Theriot (4 months ago)

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.

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?

# kai zhu (4 months ago)

as product-developer, can i ask what ux-objective you ultimately want achieved?

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, ...)
# Michael Theriot (4 months ago)

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.

# Michael Haufe (4 months ago)

To quote from a short conversation I had with Allen Wirfs-Brock:

“Proxies have a similar issue WRT the "internal slots" of built-ins. The alternatives were the non-othogonality with Proxy or private fields whose privacy was insecure. TC39 choose (correctly, I think) in favor of secure field privacy. The lesser evil choice.”

With that being said workarounds have already been presented:

tc39/proposal-class-fields#106, javascript.info/proxy#private-fields

From: es-discuss <es-discuss-bounces at mozilla.org> On Behalf Of Michael Theriot

Sent: Sunday, July 12, 2020 5:10 PM To: kai zhu <kaizhu256 at gmail.com>

Cc: es-discuss at mozilla.org Subject: Re: Why does a JavaScript class getter for a private field fail using a Proxy?

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<mailto:kaizhu256 at gmail.com>> wrote:

as product-developer, can i ask what ux-objective you ultimately want achieved?

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<mailto: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.

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<mailto: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<mailto:francois.remy.dev at outlook.com>> wrote:

At the risk of pointing out the obvious:

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<mailto:michael.lee.theriot at gmail.com>

Sent: Sunday, July 12, 2020 20:59 To: Michael Haufe<mailto:tno at thenewobjective.com>

Cc: es-discuss at mozilla.org<mailto: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.

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:

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<mailto: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:

tc39/proposal-class-fields#106

I suggest the following guide to assist you:

javascript.info/proxy#proxy-limitations

Another possible approach is to have your classes extend a proxy:

< >

From: es-discuss <es-discuss-bounces at mozilla.org<mailto: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<mailto: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

# #!/JoePea (4 months ago)

TC39 choose (correctly, I think) in favor of secure field privacy. The lesser evil choice.”

The PrivateField tunneling that was talked about in that issue would not expose any privacy. The fact that external proxies break when internal code introduces a private field is actually leaking privacy.

TC39 did not choose in favor of privacy, they chose to leave a feature out and make private fields less private than they should be.

With that being said workarounds have already been presented:

Those workaround only work in certain cases, namely when you control the proxy and then give it to external code, or if you tell all your users to make sure they write proxy membranes for all your methods (ouch!).

A true solution would leave all existing code working as-is, and would never break if internal code introduced a private field.

#!/JoePea

# #!/JoePea (4 months ago)

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

# Jordan Harband (4 months ago)

So can:

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.

# #!/JoePea (4 months ago)

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

# Jordan Harband (4 months ago)

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

# #!/JoePea (a month ago)

Gotcha. So basically that tells the consumer "No, you can't use Proxies". What I want to tell them is "Go ahead, use Proxies to your heart's content", but I don't want to tell them to write all the overly-complicated code required for their Proxies to work properly as soon as I introduce a single private field within my class.

#!/JoePea