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?

AshleysBrain
  • 22,335
  • 15
  • 88
  • 124
  • 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);` – Ivan Castellanos Nov 26 '18 at 01:20

2 Answers2

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.

Oriol
  • 274,082
  • 63
  • 437
  • 513
  • One problem with this is if `DebugCheckHandler` throws on setting non-existent properties, it will throw as the constructor adds the object properties. – AshleysBrain Dec 21 '15 at 15:50
  • @AshleysBrain 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. – Oriol Dec 21 '15 at 16:25
  • 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. – AshleysBrain Dec 21 '15 at 16:32
  • @AshleysBrain Are you OK with altering the traps after the object has been constructed? [Example](https://jsfiddle.net/emc6z55t/) – Oriol Dec 21 '15 at 16:42
  • This does not work for `class`es, 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. – Bergi Dec 22 '15 at 12:36
  • @Bergi 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]]. – Oriol Dec 22 '15 at 14:17
  • 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? – Bergi Dec 22 '15 at 14:41
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();
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I think the problem is that not all `Something` instances are supposed to be defended, though. – Oriol Dec 22 '15 at 15:07
  • 1
    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 …; }` – Bergi Dec 22 '15 at 15:10
  • 1
    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. – AshleysBrain Dec 22 '15 at 17:04
  • 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. – Bergi Dec 22 '15 at 19:10