Equality between Proxy and target object
Asked Answered
Z

2

9

Javascript's new Proxy feature offers some interesting features for debugging. For example you can "defend" an object by putting it behind a Proxy with a get handler that throws if you access an undefined property. This helps catch typos and other kinds of mistakes.

This could be used something like this:

class Something {
    constructor()
    {
        this.foo = "bar";

        allObjects.push(this);
    }
};

function defend(o)
{
    return new Proxy(o, DebugCheckHandler);
};

let rawSomething = new Something();
let defendedSomething = defend(rawSomething);

Code can be diligently written to only deal in terms of defendedSomething. However in this example, the Something constructor passes this somewhere else (to allObjects). This ends up having the same effect as using both rawSomething and defendedSomething across the codebase.

Problems then arise from the fact the proxy reference does not equal its original reference, since rawSomething !== defendedSomething. For example, allObjects.includes(defendedSomething) will return false if it contains rawSomething, because includes does a strict === check.

Is there a good way to solve this without making too many changes to the code?

Zaneta answered 21/12, 2015 at 14:56 Comment(1)
Using globals (or any external variable) inside a constructor is extremely poor engineering, the allObjects.push(this); should be outside, e.g. allObjects.push(rawSomething);Eba
V
0

Instead of letting the new operator call [[Construct]], which produces a real instance, you can:

  1. Create an object which inherits from the prototype of the constructor.
  2. Use it as the handler of the proxy.
  3. Call the constructor passing the proxy as the this value.
function Something() {
  this.foo = "bar";
  allObjects.push(this);
}
function defendIntanceOf(C) {
  var p = new Proxy(Object.create(C.prototype), DebugCheckHandler);
  C.call(p);
  return p;
};
let defendedSomething = defendIntanceOf(Something);

Note I used the function syntax instead of the class one in order to be able to use Function.call to call [[Call]] with a custom this value.

Volumed answered 21/12, 2015 at 15:7 Comment(7)
One problem with this is if DebugCheckHandler throws on setting non-existent properties, it will throw as the constructor adds the object properties.Zaneta
@Zaneta Then make it not throw? Or maybe var p = new Proxy(new C(), DebugCheckHandler) if you don't mind pushing an useless object to allObjects. Or var p = new Proxy(Object.assign(Object.create(C.prototype), {foo: void 0}), DebugCheckHandler) if you now the properties beforehand.Volumed
I'm looking for a solution that avoids the debug check handler ever finding false positives in constructors, never pushes a useless object to allObjects, and does not require completely rewriting constructors or otherwise uses a considerably different style.Zaneta
@Zaneta Are you OK with altering the traps after the object has been constructed? ExampleVolumed
This does not work for classes, which can only be [[construct]]ed but not [[call]]ed. And Reflect.construct does take a (subclass) constructor as its third argument, not an instance.Outshoot
@Outshoot Oh, thanks, it seems I misunderstood Reflect.construct; it doesn't help I don't know any working implementation of Reflect. And yes, I didn't use the class syntax in order to have [[Call]].Volumed
Oh, I first thought you could work around this by passing something crazy as newTarget but I don't think it's possible. Can you have a look at my answer and try to bust it?Outshoot
O
0

Iirc, the only way to influence the this value in a constructor - which is what you need here - is through subclassing. I believe (but can't test) that the following should work:

function Defended() {
    return new Proxy(this, DebugCheckHandler);
//                   ^^^^ will become the subclass instance
}

class Something extends Defended {
//              ^^^^^^^^^^^^^^^^ these…
    constructor() {
        super();
//      ^^^^^^^ …should be the only changes necessary
//      and `this` is now a Proxy exotic object
        this.foo = "bar";

        allObjects.push(this);
    }
};

let defendedSomething = new Something();
Outshoot answered 22/12, 2015 at 14:40 Comment(4)
I think the problem is that not all Something instances are supposed to be defended, though.Volumed
Hm, in that case you'd need to pass a boolean parameter from the Something constructor upwards to Defended, and make it like function Defendable(defend) { if (defend) return new Proxy …; }Outshoot
No, the real problem is if the DebugCheckHandler throws on adding non-existent properties (which I want it to), then the Something class constructor will throw when adding the "foo" property.Zaneta
Hm, that sounds like you really need to adapt the constructor code then and decide explicitly where you want to use the proxy (for passing around) and where the target (for initialisation). Of course you could also wrap the constructor to create a (temporal) zone during which adding properties is allowed, but that can backfire just as well.Outshoot

© 2022 - 2024 — McMap. All rights reserved.