Search code examples
javascriptes6-classes6-proxy

A proxied JS child class assigns a wrong prototype to an instance


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();


Solution

  • 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.