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:
- Attempting to access the property through a proxy without a handler throws an error
- Explicitly forwarding a handler
get
trap throughReflect.get
reinstates normal behaviour
class 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:
- Default behaviour is still broken
Reflect.get
no longer works- I can explicitly bind
this
in theget
trap - But this isn't a reinstatement of expected behaviour
class 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)
}
}