fasttime / Polytype

Dynamic multiple inheritance for JavaScript and TypeScript. Without mixins.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bind methods problem

andresilvasantos opened this issue · comments

Hi Francesco!

Thank you for Polytype, it's very well done and carefully explained. 😃

I'm experiencing the following issue, when dealing with binds in base classes.
If I use only extends in this scenario it works as expected.

import { classes } from 'js/polytype'

class TestDefault {
    constructor() {
        this.print = this.print.bind(this)

        console.log('TestDefault constructor')
    }

    print() {
        console.log('TestDefault Print')
    }
}

class Test extends classes(TestDefault) {
    constructor() {
        super()

        this.someVar = 6

        console.log('Test constructor')
    }

    print() {
        console.log('Test Print', this.someVar)

        super.print()
    }
}

const test = new Test()

test.print()

This will print:

TestDefault constructor
Test constructor
Test Print undefined
TestDefault Print

Instead of Test Print undefined, it should be be Test Print 6.

I'm guessing the binding only binds the instance of TestDefault.
I've tried to bind again print in Test, but nothing changed.

Do you know what can be done to solve this?

Thank you!

Hi André, thanks for reporting the issue. I'm glad to hear that you like this project.

The problem you noticed boils down to a very odd peculiarity of Polytype, and it's my fault that I haven't documented that yet.

The thing is that this in a base costructor is not the same this as in the rest of an object lifecycle.

Consider for example:

let thisA;
let thisB;

class A {
    constructor() {
        thisA = this;
    }
}

class B extends A {
    constructor() {
        super();
        thisB = this;
    }
}

new B();

console.log('thisA === thisB', thisA === thisB);

It outputs as expected

thisA === thisB true

But if the declaration of class B is changed to use Polytype like in
class B extends classes(A) { ... }, then thisA and thisB are no longer identical:

thisA === thisB false

Unfortunately, there's not much I can do to change this particulal behavior, as this is currently a limitation imposed by the language. There is simply no reliable way in JavaScript to invoke a class constructor on an arbitrary object; instead, it's the constructor call that creates a new object and returns it to the caller.

As for your specific problem, the expression this.print.bind(this) in the TestDefault constructor is picking the correct method (the print method of Test) but binding it to a dummy object (this in the base constructor - not the real Test instance). Properties set on the real Test instance are never reflected back to this object. This is where Polytype could probably do better: the base constructor this should be less of a dummy and more like a live proxy of the real instance.

The best workaround so far is probably to re-bind the method in the constructor of the derived class. In this way you don't even need to change the base class (which somethimes you cannot).

    class Test extends classes(TestDefault) {
        constructor() {
            super()
+           this.print = Test.prototype.print.bind(this);

            this.someVar = 6

            console.log('Test constructor')
        }

        print() {
            console.log('Test Print', this.someVar)

            super.print()
        }
    }

Another option is to bind the method after the constructor call:

    import { classes } from 'js/polytype'

    class TestDefault {
        constructor() {
-           this.print = this.print.bind(this)
-
            console.log('TestDefault constructor')
        }

+       init() {
+           this.print = this.print.bind(this)
+       }
+
        print() {
            console.log('TestDefault Print')
        }
    }

    class Test extends classes(TestDefault) {
        constructor() {
            super()

            this.someVar = 6

            console.log('Test constructor')
        }

        print() {
            console.log('Test Print', this.someVar)

            super.print()
        }
    }

    const test = new Test()
+   test.init()

    test.print()

I'll have to elaborate on the documentation and see what else can be done to improve the behavior of this in base constructor.

Thank you so much for your prompt response and explanation!

I tried your first solution and it works, but as I'm dealing with lots of inheritances and binds in my project, I created a little hack inside Polytype that helps to do that without having to worry with rebinds, changing the createConstructorTarget:

const createConstructorTarget =
typeSet =>
{
    // Added by me.
    const isBounded = (func) => {
        return func.name.startsWith('bound ')
    }

    const constructorTarget =
    function (...args)
    {
        const descriptorMapObjList = [];
        {
            const typeToSuperArgsMap = createTypeToSuperArgsMap(typeSet, args);
            const newTarget = new.target;
            for (const type of typeSet)
            {
                const superArgs = typeToSuperArgsMap.get(type) || EMPTY_ARRAY;
                const newObj = _Reflect_construct(type, superArgs, newTarget);
                const descriptorMapObj = _Object_getOwnPropertyDescriptors(newObj);
                descriptorMapObjList.push(descriptorMapObj);
            }
        }

        for (const descriptorMapObj of descriptorMapObjList) {
            // Added by me.
            for(const key of Object.keys(descriptorMapObj)) {
                const value = descriptorMapObj[key].value

                if(typeof value === 'function' && isBounded(value)) {
                    descriptorMapObj[key].value = this[key].bind(this)
                }
            }

            _Object_defineProperties(this, descriptorMapObj)
        }

        for (let descriptorMapObj; descriptorMapObj = descriptorMapObjList.pop();) {
            _Object_defineProperties(this, descriptorMapObj);
        }
    };
    _Object_setPrototypeOf(constructorTarget, null);
    return constructorTarget;
};

However, I don't know if this is the proper way to do it without suffering any future issues.
But for now, it seems to work as expected for my use case.

Thank you once again!

I'm afraid your fix will not work in a general case because it is not immediately possible to tell if a method was bound to this or to another value. In order to do that, either Function.prototype.bind must be spied upon, or the method itself must be wrapped into a caller that replaces this if necessary. There are certainly other options, too. I'll be back to you when I come up with a solution to this problem.

Yes, you're totally right. If my limited JS knowledge allows me to find a better solution, I'll share! 😄

Thank you for your insights and I wish you an awesome weekend!

This issue has been tackled in the current version (0.13.2) by stubbing the bind method. The current solution is far from perfect because it only works for bound functions (not for closures) and only when a superconstructor dummy substitute is used as the value of this in a call to bind (and not bound as an argument, or provided by some other means).
The current approach should still solve your specific problem.
I also added a section to the documentation to hopefully help other users.
Thanks again for reporting the issue and please, let me know if you have any more question.