Reputation: 16456
I'm trying to proxy a set of object such that I can pass them off to third party code and temporarily nullify mutation methods and setters, and then revoke the proxy handler traps to reinstate normal behaviour. What I've discovered is that proxies are inherently hostile to this
-dependent code.
I'm intrigued as to how & why Javascript Proxies break this
binding for their proxied targets. In the following example I have a simple class which ingests a value on construction, stores it as a private field, and returns it on property access. Observe that:
get
trap through Reflect.get
reinstates normal behaviourclass Thing {
#value
constructor(value){
this.#value = value
}
get value(){
return this.#value
}
}
// Default behaviour
const thing1 = new Thing('foo')
attempt(() => thing1.value)
// No-op proxy breaks contextual access behaviour
const proxy1 = new Proxy(thing1, {})
attempt(() => proxy1.value)
// Reinstated by explicitly forwarding handler get call to Reflect
const proxy2 = new Proxy(thing1, {get: (target, key) =>
Reflect.get(target, key)
})
attempt(() => proxy2.value)
function attempt(fn){
try {
console.log(fn())
}
catch(e){
console.error(e)
}
}
This offers a workaround for getter access, but I don't understand why the problem arises or why the extra code fixes it. The same problem of context clash in unhandled proxy queries is more vexing when it comes to methods. In the following code, the value
property becomes a method rather than a getter. In this case:
Reflect.get
no longer worksthis
in the get
trapclass Thing {
#value
constructor(value){
this.#value = value
}
value(){
return this.#value
}
}
// Default behaviour
const thing1 = new Thing('foo')
attempt(() => thing1.value())
// No-op proxy breaks contextual access behaviour
const proxy1 = new Proxy(thing1, {})
attempt(() => proxy1.value())
// Forwarding handler get trap to Reflect doesn't work
const proxy2 = new Proxy(thing1, {get: (target, key) =>
Reflect.get(target, key)
})
attempt(() => proxy2.value())
// Explicitly binding the returned method *does* work
const proxy3 = new Proxy(thing1, {get: (target, key) =>
target[key].bind(target)
})
attempt(() => proxy3.value())
// But this goes beyond reinstating normal behaviour
var {value} = thing1
attempt(() => value())
var {value} = proxy3
attempt(() => value())
function attempt(fn){
try {
console.log(fn())
}
catch(e){
console.error(e)
}
}
Upvotes: 3
Views: 1585
Reputation: 14165
TDLR;
The no-op Proxy in the first example is not broken. The get()
method is still invoked via the Proxy object (even no-op) and not the thing
object. So the private member is not accessible via the proxy with proxy1.value
. Your fix in the first example, using reflection, is the common way of accessing those members in almost every language with access restrictions (some require inflection). Historically, before Reflect.get()
was available, this was done using the function object's .apply()
method. So the use of Reflect.get()
makes sense for the very same reason.
Bottom Line:
So you must take some action to set the context to the object that created the private member, otherwise you will not gain access to it.
The call to Reflect.get()
doesn't work in the second example because of shifting getter syntax from get value()
to value()
. Now that a function is called to retrieve value
it must be bound to the correct object. Simple reflection is not sufficient. To make Reflect.get()
work here you must bind the getter function to the target.
Using function's .bind()
method is the other traditional way of controlling the context of operation. From the docs:
The bind() method creates a new function that, when called, has its
this
keyword set to the provided value...
Reflect.get(target, key).bind(target)
Which is exactly the same as your use of .bind()
in this:
target[key].bind(target)
The static Reflect.get() method works like getting a property from an object (target[propertyKey]) as a function.
Bottom Line
In both cases (Reflect.get()
, .bind()
), the context is shifted to the object that created the private member. This is necessary in many use cases and is not isolated to Proxy.
class Thing {
#value
constructor(value) { this.#value = value }
value() { return this.#value }
get value() { return this.#value; }
set value(v) { this.#value = v; }
someMethod() { return 'Cannot get here when proxied.'}
}
const thing = new Thing('foo')
const revokeMe = Proxy.revocable(thing, {
get: (target, key) => {
if (key === 'value') {
return () => 'value is undefined (blocked by proxy)'
}
if(key === 'someMethod') {
return () => `cannot invoke ${key}. (blocked by proxy)`;
}
return Reflect.get(target, key).bind(target);
},
set: (target, key, value) => {
if (key === 'value') {
console.log(`cannot set ${key} property. (blocked by proxy)`);
}
return Reflect.set(target, key, value);
}
});
const proxy = revokeMe.proxy;
console.log(proxy.value());
proxy.value = 'test';
console.log(proxy.value());
console.log(proxy.someMethod());
revokeMe.revoke();
try {
proxy.value();
} catch (err) {
console.log('proxy has been revoked');
}
thing.value = 'new value';
console.log(thing.value);
console.log(thing.someMethod());
Focusing on this problem statement: "temporarily nullify mutation methods and setters, and then [...] reinstate normal behaviour."
The solution doesn't require a Proxy at all given the code you provided. Simply set the prototype of the object in question and override the properties/methods as necessary.
class Thing {
#value
constructor(value){ this.#value = value + ' cannot get here'}
value(){ return this.#value + ' not gonna happen'}
get value(){ return this.#value + ' not gonna happen'}
set value(v) { this.#value = value;};
toBeDisabled() { return 'will not show';}
}
class Overrides {
constructor(value) {
this.value = value + ' (set in Overrides)';
}
get value() {return 'value is undefined (set in Overrides)';}
set value(v) {}
toBeDisabled(){
return 'NoOp (in Overrides)';
}
}
let thing = new Thing('foo');
thing.__proto__ = new Overrides(thing.value);
thing.value = 'new value';
console.log(thing.value);
console.log(thing.toBeDisabled())
thing.__proto__ = {}
thing.value = 'now value will set; proxy is disabled;';
console.log(thing.value);
Upvotes: 4