JS Tip of the Day: A Function's Home Object

A Function’s Home Object
Version: ES2015
Level: Advanced

All JavaScript functions have a internal slot called [[HomeObject]]. This is used to store a function’s “home object” which is the object in which the function was originally defined. This doesn’t get set for all functions, though, only method-style functions defined in classes or object literals. All other functions have a [[HomeObject]] value of undefined.

let obj = {
    method () {}
};
// obj.method.[[HomeObject]] = obj;

class Obj {
    method () {}
}
// Obj.prototype.method.[[HomeObject]] = Obj.prototype;

function func () {}
// func.[[HomeObject]] = undefined;

Home objects are used by super to know how to make super method calls. When a super method is called, the runtime looks at the [[HomeObject]] of the current function, gets the prototype of that object, then calls the super method from there. This ensures the method getting called is inherited and not from the current object or its immediate prototype.

class Sup {
    method () {
        console.log('Sup method called');
    }
}

class Sub extends Sup {
    method () {
        console.log('Sub method called');
        super.method();
        // called as:
        // Object.getPrototypeOf(this.method.[[HomeObject]])
        //    .method.call(this);
    }
}
new Sub().method();
/* logs:
Sub method called
Sup method called
*/

For the most part, all of this works transparently and super method calls magically work as they should. However, the way they’re called through the home object could cause problems, especially if you’re trying to use methods relying on super in, for example, non-class-based mixins.

Consider what would happen if you created a class of only methods, each of which that you wanted to copy into another object or class prototype by direct copy. If any of those methods used super, you may not get the behavior you expected.

// class containing methods to mix in
class SharedMethods {
    method () {
        console.log('Shared-to-be-Sub method called');
        super.method();
    }
}

// class hierarchy receiving the mixin
class Sup {
    method () {
        console.log('Sup method called');
    }
}
class Sub extends Sup {}
Reflect.ownKeys(SharedMethods.prototype) // mix in
    .filter(key => key !== 'constructor')
    .forEach(key => {
        Object.defineProperty(
            Sub.prototype,
            key,
            Object.getOwnPropertyDescriptor(
                SharedMethods.prototype,
                key
            )
        )
    });

new Sub().method(); // Shared-to-be-Sub method called
// TypeError: method is not a function

While one might expect super.method() from the mixin - now part of Sub.prototype - to reach up and call into Sup.prototype.method(), this is not the case. In order to find super.method(), the called function uses its home object, a value created when it was originally defined and one that will not change when the function is reassigned by the mixin. The home object of that mixed in method() function, because it was originally created in the SharedMethods class, is SharedMethods.prototype. When super.method() is attempted from within it, it will always look for the “method” function in the prototype of SharedMethods.prototype, which here is Object.prototype. And since method() doesn’t exist there, an error is thrown.

There is no way for super to work in these kinds of mixins. Class-based mixins do work, however, because each mixin class get its own copy of the method, each with their own home object that can correctly target the appropriate superclass.

function shared (Base) {
    return class extends Base {
        method () {
            console.log('shared-to-be-Sub method called');
            super.method(); // finds through Base
        }
    }
}

class Sup {
    method () {
        console.log('Sup method called');
    }
}

class Sub extends shared(Sup) {} // mix in
new Sub().method();
/* logs:
shared-to-be-Sub method called
Sup method called
*/

More info:


More tips: JavaScript Tips of the Day