Performance comparison of JavaScript class inheritance patterns

"JavaScript is a beautiful and expressive language." Everybody says it, and it's true! The downside, of course, of such a flexible language is that there are 1001 ways to do everything, and not all ways are created equal. The performance (or lack thereof) of different patterns can be surprising, as well as their implementation quirks.

JavaScript's various patterns for object-oriented-programming comprise an especially vibrant topic. Many JavaScript developers have their own Best Way to implement JavaScript classes and inheritance. Many others will simply use existing libraries exist to facilitate the task. I've looked at a lot of different patterns, analyzed their performance, took into account their convenience, and came up with what I think is the simplest, most practical and most performant approach.

In other words, my Best Way. ;-)

Where Were They Going Without Ever Knowing The Way(s)

I'll try and broadly categorize the most common approaches out there that I've encountered. Keep in mind the dynamics of the language are such that even within these categories, there are still many popular variations, and so these categories are by no means thorough, but for performance comparisons it won't make much of a difference.

Closures

Perhaps the most "old school" pattern, closures allow truly private variables and simple inheritance. In the closure method, you define a new class by defining a constructor function which defines your class properties and methods on a local object variable and returns that object, effectively creating a new instance of a class.

function ClassConstructor(privateValue, publicValue) {
    var classObject = {};
    
    var privateProperty = privateValue;
    
    classObject.publicProperty = publicValue;
    
    classObject.getPrivateProperty = function () { 
        return privateProperty;
    };
    
    classObject.method = function (arg) {
        // Private properties are not actually properties of the object we are
        // creating to represent our class, but nonetheless they are accessible
        // to the instance of this class we are returning with this function
        // (and ONLY to THAT instance) because of the closure. Refer to them in
        // methods just with their names.
        privateProperty += arg;
        
        // Refer to public properties with the 'this' keyword.
        this.publicProperty += arg;
    };
    
    return classObject;
}     

And so, it's simple to create a new instance of a class, and the rest works as expected.

var instance = ClassConstructor('hidden', 'public');

instance.method('argument');

// Returns undefined!
console.log(instance.privateProperty);

// Returns 'publicargument'
console.log(instance.publicProperty);

// Returns 'privateargument'
console.log(instance.getPrivateProperty());

Inheritance in the closure format is also quite simple. If you're creating a child class, call the parent class's constructor in the child class's constructor, and extend that instance with your child class's additional properties and methods.

function ChildClassConstructor(privateString, publicString, childClassProp) {
    var childClassObject = classConstructor(privateString, publicString);
    
    childClassObject.childClassPublicProperty = childClassProp;
    
    childClassObject.childClassPublicMethod = function () {
        this.privateProperty += childClassProp;
    }
    
    return childClassObject;
}    

The closure method has elegantly clean code in my opinion. If you know basic JavaScript, it makes perfect sense, and of course implements private members beautifully. Additionally, the code that defines a new class runs extremely fast, because all you are doing is defining a function.

Unfortunately, that's only relevant if you're defining a lot of new classes at runtime, which will cause JIT optimizers to hate you anyway. The real upshot of having very little work to do to define class, is that there's more work to do to actually instantiate a new object of that class. As you can see, the closure method is an awful performer compared to the other, more browser-optimized approaches.

"New" School

The next category I want to talk about employs the new operator. Browsers really like new because the objects it's creating are already defined via a constructor and its prototype property. They know exactly how to make new objects of that type. When we create objects with the closure method, we're doing so from the ground up, defining each property and method one at a time, each time an instance is created. Intuitively, we know those objects are going to be the same because we've defined the procedure to make them, but browsers much prefer a sort of, "blueprint object" to copy. It's the difference between instance creation taking 1 line of code (with the new operator), or any number of lines of code (with the closure method), depending on the complexity of the class.

Let's take a look at how to define classes via a constructor and its prototype property, for use with new.

// Note the name of this function is also used as the name of our class

function SomeClass(value1, value2) {
    // We use 'this' because unlike our closure constructor, this function is
    // going to be called with the new instance of our object as the invocation
    // context. So 'this' is referring to that new instance, and we'll use it to
    // define and set the value's of the properties of this class to values of 
    // the arguments passed to the constructor. Note that this means when you
    // define properties here they are not part of the prototype, but local to 
    // each instance. For local properties, this is what we want.
    
    this.property1 = value1;
    this.property2 = value2;
}

// Methods, on the other hand, should be defined on the prototype property, as 
// they won't be changing from instance to instance.

SomeClass.prototype.method = function (arg) {
    this.property1 += arg;
    this.property2 += arg;
};

And (drumroll), time for new to do its magic! Here's how to create a new instance of our generic, "SomeClass" type.

var instanceOfSomeClass = new SomeClass(123, 456);

Tada! When we use new this way, two things happen. First, a new object is created with the value of the prototype property of SomeClass as its prototype. Then, it calls the constructor function, SomeClass, with the arguments we pass to it, and that new object as the invocation context for that function. Finally, the new instance is assigned to our instanceOfSomeClass variable. And this all happens really fast.

You Are The Prototype

Closures implement inheritance very simply: just by making copies of objects and extending them. When we use the new operator, we'll make use of JavaScript's prototypal inheritance model. You're probably already familiar with it, but just in case it's basically this: every object has an associated prototype object. When looking up a property or method on an object, the interpreter will first check the object for that property or method of course, but if it's not found it'll then look to its prototype. If it's still not found, it'll check the prototype's prototype, and so on, until it's found or it's reached the end of the prototype chain (the generic Object.prototype, who's own prototype is null).

To create an instance of a subclass, we need the prototype chain of that instance to look up the subclass, then the parent class. An instance get's its prototype from the prototype property of its class's constructor, and so, we need that prototype property to have it's own prototype, and it should be the prototype property of it's parent class's constructor. This is analogous to how we implemented inheritance with closures. Essentially, we'll start with a previous object, and extend it. Except in this case we're effectively dealing with prototypes: we start with a parent class's prototype, and then extend it.

Referencing a method or property of an instance of the child class will check out that object first, then it's prototype (the child class), and then the its prototype's prototype (the parent class), stopping where ever the property or method is found first. That's a mouthful. Let's check out some code.

// Start just as before with a normal class definition, defining properties in
// the constructor, and methods on the prototype of that constructor.

function ParentClass(x,y) {
    this.x = x;
    this.y = y;
}

// Now extend the prototype *property* of the constructor with some methods.
ParentClass.prototype.add = function (x,y) {
    this.x += x;
    this.y += y;
};

// Now let's define the constructor of our child class.

function ChildClass(x,y,z) {
    // Okay, so when a new instance of ChildClass is created it's going to have
    // the prototype chain taken care of, but what about the constructors?
    // There's still relevant properties and initialization to take care of
    // there! Welp, it's less than glamorous, but we just have to call it
    // ourselves, using 'this' as the invocation context. Remember, 
    // constructors are called with the new 'instance' of the class as the 
    // invocation context, so with 'this' we're just passing that along with the
    // relevant arguments.
    
    ParentClass.call(this, x, y);
    
    // Initialize the properties new to the child class
    this.z = z;
}

// Now here's where we inherit the prototype of the parent class. Notice we use
// 'new' here because we want to modify the actual prototype of the prototype
// property. Right now the only mechanism we have to do that is with 'new',
// which returns a new object with the prototype equal to the operand's 
// (parentClass's) prototype property.

ChildClass.prototype = new ParentClass();

// Now, this overwrites something important to us. That prototype property has a
// 'constructor' property. Actually, every object does. And 'new' uses it. When 
// we overwrite the prototype property entirely this way, we're also overwriting 
// the constructor property that would have been === ChildClass. No workaround
// but to fix it manually.

ChildClass.prototype.constructor = ChildClass;

// Now we can extend the prototype as normal, overriding parentClass methods
// with new ones of the same name (to be found first in the prototype chain),
// or additional, unique methods for the ChildClass.

ChildClass.prototype.add = function (x, y, z) {
    this.z += z;
    
    // In methods, calling the parent class's version works the same way as in
    // the constructor.
    ParentClass.prototype.add.call(this, x, y);
};

Alright! We have fully functioning object-oriented approach for JavaScript, complete with inheritance, that is very fast. Now for the finale.

My Way Can Beat Up Your Way

Okay, we're just about done, but let's take a look at a few issues with our latest approach, and get to the specifics of my Way.

First off, when we are creating an object with new, we know that this calls the constructor function, in addition to spawning a new object with a prototype equal to that constructor's prototype property. When we are setting up the prototype property of a child class's constructor, we want the object with the right prototype, but we don't want to call that constructor. We didn't even pass any arguments. And what arguments would we pass at that point? Worse, if you have some heavy intialization code in your parent class's constructor that does more than just assign those arguments to properties, it probably won't even work at all.

Luckily, there's a really easy way around this for modern browsers (>IE8). Instead of using new parentConstructor() use Object.create(parentConstructor.prototype). This does the same exact thing as new, except it doesn't call the constructor function, and it accepts the object-to-be-used-as-a-prototype as an argument directly, instead of a constructor function. It can also do some fancy stuff with a second argument, though it's not really relevant to this post. The only downside is that Object.create is about half as fast as new, but since it's only called once per subclass definition, I don't imagine that ever outweighing its benefits.

Secondly, we can abstract away some of the details for ourselves by making a sugar function to create a child class.

function createChildClass(Child, Parent) {
    // What we just talked about.
    Child.prototype = Object.create(Parent.prototype);
    
    // Now let's add a reference to the parent class's prototype property to the
    // Child class for easy referencing.
    Child._parent = Parent.prototype;
    
    // Overwrite the constructor property as before.
    Child.prototype.constructor = Child;
}

Usage:

function ChildClass (x, y, z) {
    // Use our _parent property defined as a property of ChildClass. Save some
    // characters and time. Notice because _parent refers to the prototype
    // property of the constructor and not the constructor of the class itself,
    // we have to explicitly look up the constructor like so. 
    ChildClass._parent.constructor.call(this, x, y);
    
    this.z = z;
}

// This makes the magic happen.
createChildClass(ChildClass, ParentClass);

// Extend the prototype property...
ChildClass.prototype.method = function (...) {
    // Do stuff
    
    // Call the parent method
    ChildClass._parent.method.call(this, ...);
};

"No, Your Way Can't Beat Up My Way"

Well, maybe. My Way will outrun yours, though. There are a gazillion features that you could implement from here, yes, but the thing is, anything extension or tweak from here is almost definitely going to perform significantly worse (aside from adding static private members, which is easy and very performant via a closure around the methods defined on a prototype property). Now, performance might not be a huge deal depending on your project, say if you're not instantiating classes very often. If that's the case, there are definitely some excellent features you can add that will help prevent bugs and abstract away even more of the implementation details. But, if you're looking for performance, and ECMAScript 6 classes aren't yet implemented, my Way is Best ;-).

Comments

Popular posts from this blog

What the health!? Implementing health probes for highly available, self-healing, global services

Asynchronous denormalization and transactional messaging with MongoDB change streams

A Case Study of Java's Dynamic Proxies (and other Reflection Bits): Selenium's PageFactory