0

Some interesting case which I don't understand. I proxy classes and extend a child class from a proxied base class.

When a child is constructed inside the construct trap for some reason a wrong prototype is assigned to an instance - the base class' prototype instead of a child class' prototype:

class prototype: Child [
  "constructor",
  "method",
  "childMethod"
]
assigned prototype: Base [
  "constructor",
  "method"
]

This happens both in Chrome and Firefox. So it's not looking like a bug but rather everything to the spec. The problem I cannot understand why. The fix is to set the prototype manually (the commented line), but the mystery remains.

Could anyone explain why this happens:

const proxy = what => new Proxy(what, {

    construct(_class, args, constructor) {

        const obj = new _class(...args);
        
        console.log('class prototype:', _class.name, Object.getOwnPropertyNames(_class.prototype));
        console.log('assigned prototype:', obj.__proto__.constructor.name, Object.getOwnPropertyNames(obj.__proto__));
        
        // for some reason we need this if a class is proxied
        //Object.setPrototypeOf(obj, _class.prototype);

        return obj;
    }
});

const Base = proxy(class Base {
    isBase = true;
    method(){
      console.log('Base method');
    }
});

const Child = proxy(class Child extends Base { // extends from a proxy

    isChild = true;
    method() {
        console.log('Child method');
        super.method();
    }
    childMethod(){}

});

const base = new Base;
const child = new Child;

console.log('--------- EXECUTING METHODS ---------');
base.method();
child.method();

If we set the prototype manually everything works fine:

const proxy = what => new Proxy(what, {

    construct(_class, args, constructor) {

        const obj = new _class(...args);
        
        // for some reason we need this if a class is proxied
        Object.setPrototypeOf(obj, _class.prototype);

        return obj;
    }
});

const Base = proxy(class Base {
    isBase = true;
    method(){
      console.log('Base method');
    }
});

const Child = proxy(class Child extends Base { // extends from a proxy

    isChild = true;
    method() {
        console.log('Child method');
        super.method();
    }
    childMethod(){}

});

const base = new Base;
const child = new Child;

console.log('--------- EXECUTING METHODS ---------');
base.method();
child.method();
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
  • 3
    Not entirely sure what happens exactly, hence not posting an answer, but note that your trap is actually called both for the Base parent class and for the Child one (So it's called in total 3 times in your snippet, accounting for the first `Base=`). You never use the `constructor` param and for both calls, the `target` will be the `Base` class. But the `constructor` is different. What you need here is `Reflect.construct(_class, args, constructor)` so that you can set the `new.target` as expected. – Kaiido Aug 09 '23 at 11:48
  • @Kaiido damn, yesterday i thought i should try `Reflect.construct()` today but forgot to do that, silly... `Reflect.construct()` works – Alexander Nenashev Aug 09 '23 at 12:51
  • 2
    "*I proxy classes*" - but why? – Bergi Aug 09 '23 at 13:11
  • @Bergi purely for learning JS and experimenting (you won btw ) https://stackoverflow.com/questions/76861645/whats-the-correct-way-to-declare-event-handler-in-class-of-javascript/76861845#76861845 – Alexander Nenashev Aug 09 '23 at 13:29
  • @AlexanderNenashev I see - for autobind, you should however use a decorator (that wraps the constructor or transforms prototype methods into getters), not a proxy that would intercept any property access – Bergi Aug 09 '23 at 13:51
  • @Bergi the proxies are only for the new operator. the rest is decorators like you expected – Alexander Nenashev Aug 09 '23 at 14:03

1 Answers1

2

super() is expected to set this to an instance of the original (top-level) constructor it is called from, but this does not happen in your scenario. If in the constructor of Child you do this:

constructor() {
    super();
    console.log(this instanceof Child);
}

You'll get as output false. This happens because the proxy of the Base constructor is invoked by super() and it explicitly returns an instance of Base without any clue that this was actually intended to be a Child instance.

As already explained in comments, the correct execution of the original intent of super() is to use Reflect.construct with the third argument in your proxy handler. That handler gets a third argument that tells you what the intended type was of constructed instance:

    construct(_class, args, constructor) {
        return Reflect.construct(_class, args, constructor);
    }

Now that super() call will use that returned Child instance and set this to it.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • i guess the key is to understand `new.target` https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target which is passed as the third argument to the trap – Alexander Nenashev Aug 09 '23 at 13:27