TypeError when assigning shallowly cloned object with a method that calls super

Viewed 121

I'm testing the following code in Chrome Canary 63 and on Chrome 61. The goal of the code is to set a method that calls super onto a class instance (z). In my codebase sometimes property blocks are cloned with Object.assign before being added to the instance and whenever this happens the code fails.

I've reproduced the problem with the code example below. The code works fine if avoid the shallow clone (props = Object.assign({}, props);) but if I add that line I get TypeError: (intermediate value).bar is not a function.

I've tried doing Object.create(this, Object.getOwnPropertyDescriptors(props)) instead of Object.assign but it results in the same error.

Is there any way to properly set super on objects that have been cloned?

let Moo = class {
    bar() {
        return " [bar on Moo] ";
    }
};

let Zoo = class extends Moo { 
    bar() { 
        return " [bar on Zoo] " + super.bar();
    }
};

function addProps(inst, props) {
    // Code works if the line below is commented out but otherwise
    // results in TypeError: (intermediate value).bar is not a function
    props = Object.assign({}, props); // <-- Offending code

    Object.setPrototypeOf(props, inst.__proto__);
    Object.assign(inst, props);
}

let props = {
    onZooInst: {
        bar() {
            return " [bar on overridden instance] " + super.bar();
        }
    }
};

let z = new Zoo();
addProps(z, props.onZooInst);
console.log(z.bar());
3 Answers

Is there any way to properly set super on objects that have been cloned?

Nope. super uses the function's [[HomeObject]] slot, which is set when the function (method) is created and cannot be changed at present.

Probably the best quote from the spec on this is in Table 27:

[[HomeObject]]: If the function uses super, this is the object whose [[GetPrototypeOf]] provides the object where super property lookups begin.

The only thing you could do is try to change the prototype of the object that the function's [[HomeObject]] refers to, but that A) Gets ugly fast, and B) Kills performance (in those situations where it matters).

If you're doing lots of "method" cloning, you're better off using simple properties that refer to functions, not methods, and not using super.

If interpret requirement correctly you can adjust bar function at props object to this.bar() and use Function.prototype.call(), Function.prototype.apply() or Reflect.apply() to set this of function call to props.onZooInst.bar to Moo.prototype

let Moo = class {
    bar() {
        return " [bar on Moo] ";
    }
};

let Zoo = class extends Moo { 
    bar() { 
        return " [bar on Zoo] " + super.bar();
    }
};

let props = {
    onZooInst: {
        bar() {
            return " [bar on overridden instance] " + this.bar();
        }
    }
};

let z = new Zoo();

let reflectBar = Reflect.apply(props.onZooInst.bar, z.__proto__, []);

let callBar = props.onZooInst.bar.call(z.__proto__);

console.log(reflectBar);

console.log(callBar);

To anyone who may face the same problem I finally found a solution.

Instead of defining blocks of object literals like

let a = {
    someFunc() {
        super.someFunc();
    } 
}

You can replace them with an arrow function that returns them:

let a = () => {
    return {
        someFunc() {
            super.someFunc();
        } 
    }
};

this works because a().someFunc !== a().someFunc - in other words you get a new function and we don't need to resolve to dynamic code generation (eval/new Function). You should safely be able to do Object.setPrototypeOf on the return value of a().

Related