"JavaScript: The Good Parts"-way to implement prototype methods?

Viewed 187

Having read this article https://www.toptal.com/javascript/es6-class-chaos-keeps-js-developer-up and subsequently "JavaScript: The Good Parts", I shall henceforth commit to becoming a better JavaScript developer. However, one question remained for me. I usually implemented methods like this:

function MyClass(){
    this.myData = 43;
    this.getDataFromObject = function(){
        return this.myData;
    }
}

MyClass.prototype.getDataFromPrototype = function(){
    return this.myData;
}

var myObject = new MyClass();
console.log(myObject.getDataFromObject());
console.log(myObject.getDataFromPrototype());

My assumption that underlies this whole post is that getDataFromObject is faster (during call, not during object creation) because it saves an indirection to the prototype but it is also less memory-efficient because every object gets an own instance of the function object. If that is already wrong, please correct me and you can probably stop reading here.

Else: Both article and book recommend a style like this:

function secretFactory() {
    const secret = "Favor composition over inheritance [...]!"
    const spillTheBeans = () => console.log(secret)

    return {
        spillTheBeans
    }
}

const leaker = secretFactory()
leaker.spillTheBeans()

(quote from the article, the book didn't have ES6 yet but the ideas are similar)

My issue is this:

const leaker1 = secretFactory()
const leaker2 = secretFactory()

console.log(leaker1.spillTheBeans === leaker2.spillTheBeans) // false

Do I not mostly want to avoid that every object gets an own instance of every method? It might be insignificant here but if spillTheBeans is more complicated and I create a bazillion objects, each with twelvetythousand other methods?

If so, what is the "goot parts"-solution? My assumption would be:

const spillStaticBeans = () => console.log("Tabs rule!")
const spillInstanceBeans = (beans) => console.log(beans)

function secretFactory() {
    const secret = "Favor composition over inheritance [...]!"

    return{
        spillStaticBeans,
        spillInstanceBeans: () => spillInstanceBeans(secret)
    }
}

const leaker1 = secretFactory()
const leaker2 = secretFactory()

leaker1.spillStaticBeans()
leaker2.spillInstanceBeans()

console.log(leaker1.spillStaticBeans === leaker2.spillStaticBeans) // true
console.log(leaker1.spillInstanceBeans === leaker2.spillInstanceBeans) // false

The spillInstanceBeans method is still different because each instance needs its own closure but at least they just wrap a reference to the same function object which contains all the expensiveness.

But now I have to write every method name two to three times. Worse, I clutter the namespace with public spillStaticBeans and spillInstanceBeans functions. In order to mitigate the latter, I could write a meta factory module:

const secretFactory = (function(){

    const spillStaticBeans = () => console.log("Tabs rule!")
    const spillInstanceBeans = (beans) => console.log(beans)

    return function() {
        const secret = "Favor composition over inheritance [...]!"

        return{
            spillStaticBeans,
            spillInstanceBeans: () => spillInstanceBeans(secret)
        }
    }

}())

This can be used the same way as before but now the methods are hidden in a closure. However, it gets a bit confusing. Using ES6 modules, I could also leave them in module scope and just not export them. But is this the way to go?

Or am I mistaken in general and JavaScript's internal function representation takes care of all this and there is not actually a problem?

2 Answers

My assumption that underlies this whole post is that getDataFromObject is faster to call than getDataFromPrototype because it saves an indirection to the prototype

No. Engines are very good at optimising the prototype indirection. The instance.getDataFromPrototype always resolves to the same method for instances of the same class, and engines can take advantage of that. See this article for details.

Do I not mostly want to avoid that every object gets an own instance of every method? It might be insignificant here

Yes. In most of the cases, it actually is insignificant. So write your objects with methods using whatever style you prefer. Only if you actually measure a performance bottleneck, reconsider the cases where you are creating many instances.

Using ES6 modules, I could also leave them in module scope and just not export them. But is this the way to go?

Yes, that's a sensible solution. However, there's no good reason to extract spillInstanceBeans to the static scope, just leave it where it was - you have to create a closure over the secret anyway.

The spillInstanceBeans method is still different because each instance needs its own closure but at least they just wrap a reference to the same function object which contains all the expensiveness.

It should be noted that you're just replicating the way the JavaScript VM works internally: a function like spillTheBeans is compiled only once where it occurs in the source code even if it has free variables like secret. In SpiderMonkey for example, the result is called a »proto-function« (not to be confused with prototype). These are internal to the VM and cannot be accessed from JavaScript.

At runtime, function objects are created by binding the free variables of proto-functions to (a part of) the current scope, pretty much like your spillInstanceBeans example.

Saying that, it's true that using closures instead of prototype methods and this creates more function objects overall – the robustness gained from true privacy and read-only properties might make it worthwhile. The proposed style focuses more on objects rather than classes, so a different design could emerge that cannot be compared directly to a class-based design.

As Bergi says, measure and reconsider if performance is more important in (some part of) your code.

Related