CommonLounge Archive

Object Prototypes and Constructors

March 19, 2018

Introduction

The prototype chain is the bread and butter of JavaScript, and it’s been around since the inception of the language in 1997 (in the Standard ECMA-262).

It might be just as surprising to know that JavaScript prototypes are likely some of the most widely misunderstood topics. And in recent years, the introduction of class syntax has blurred the functional role of prototypes even more.

In this guide, we’ll walk you through the purpose of JavaScript’s prototypes, including how they work, and explain the difference between each of the various ways to construct objects (and classes) in JavaScript.

JavaScript’s prototype vs classical inheritance

In traditional programming, to help reduce the amount of code that a developer needs, a systematic structure of inheritance also known as class inheritance was devised. In this model, developers will break down components and data structures into their smallest possible size, and extend them as needed.

To put it as simply as possible, in classical inheritance there is typically a 1 to 1 correlation between a constructor and its ancestor (or the class it’s extending), but the JavaScript prototype allows a 1 to n correlations, so that a constructor can inherit multiple classes and object models at once.

In other words, JavaScript is a prototype-based language.

The Prototype Chain

Let’s take a snippet straight from the original 1997 ECMA-262 spec we linked above:

4.2.1 Objects: ECMAScript does not contain proper classes such as those in C++, Smalltalk, or Java, but rather, supports constructors which create objects by executing code that allocates storage for the objects and initializes all or part of them by assigning initial values to their properties.

To extend on that (since that was 20 years ago), the important thing to take away is that the compiler will map out a object storage structure or a template for the properties and methods of the class. This is also known as the object prototype. Every object in JavaScript has a prototype. The references created between these objects when creating more complex objects and classes is called the prototype chain.

Let’s do a basic example with a function constructor:

// define the head ancestor
function Vegetable(name, color, seedCount = 0){
   this.name = name;
    this.color = color;
    this.seeds = seedCount;
}
// add a method that returns true if it has seeds
Vegetable.prototype.hasSeeds = function(){
    return this.seeds > 0;
}
// create the object from the class
const veggie = new Vegetable('tomato', 'red', 10);
console.log(veggie, veggie.hasSeeds());
// prints Vegetable { name: 'tomato', color: 'red', seeds: 10 } true

Lets go over the Vegetable.prototype.hasSeeds line. When creating the instance (which happens at the new keyword), the object instance being created goes through two processes:

  1. The JavaScript engine will map out the class or constructor’s prototype and include any methods, values, or parent objects referenced within it.
  2. Runs the constructor code and sets (or extends) that object’s default values.

It’s important to note that since the prototype is an Object, and since a Function is also an Object (that are passed by reference), setting either on the prototype means only one reference to that method, or object will be created. Passing an object or function directly to this will cause a new one to be constructed each time you make a new object or class.

Let’s see an example of this below:

function Vegetable(name, color, seedCount = 0){
    // bad - creates a new function object on each new Object
    this.hasSeeds = function(){
        return this.seeds > 0;
    }
}
// good - create a single reference to this method for all objects that inherit it
Vegetable.prototype.hasSeeds = function(){
    return this.seeds > 0;
}

So, now that we are able to create a Vegetable, we should also be able to create Tomatoes as well. Below is a basic way to use Object.assign to generate a simplistic Functional inheritance of all of a Vegetable’s methods and it’s constructor.

function Tomato(seedCount = 10, vineCount = 0){
    // re-initialize with the Vegetable constructor
    Vegetable.call(this, 'Tomato', 'red', seedCount);
    // custom props
    this.vines = vineCount;
}
// assign the prototype of Vegetable to the Tomato
Tomato.prototype = Object.assign({
    // add a method specific to tomatoes
    hasVines(){
        return this.vines > 0;
    },
}, Vegetable.prototype);
// you can add any additional prototypes or methods you need here,
// since in JS it's routed by reference
// make a Tomato
const tomato = new Tomato(10);
console.log(tomato);
// { name: 'Tomato', color: 'red', seeds: 10, vines: 0 }
console.log(tomato instanceof Tomato); // true
console.log(tomato instanceof Vegetable); // false (since it inherits)
console.log(tomato.hasSeeds() ? 'has seeds' : 'no seeds'); // has seeds
console.log(tomato.hasVines() ? 'has vines' : 'no vines'); // no vines

Thanks to the ease of ES2015+ syntax, we are now able to use Object methods like Object.assign which make it easier to assign and overwrite the prototype without needing to re-invent the JavaScript wheel.

An interesting thing to note is how even though a Tomato inherits the Vegetable, as far as instanceof is concerned its type Tomato. The constructing function calls the Vegetable constructor and applies it directly to the Tomato’s context (or this value), so any of the defaults of the Vegetable are also applied to the Tomato. Because its applied within the context of the constructor, the class retains its custom name despite inheriting (or extending) another.

Also, as a functional tip, having the prototypes of the objects you are inheriting near the end of the Object.assign call means that any methods described in the new prototype will override the inherited ones.

Because of the nature of JavaScript objects (being passed around by reference), there can sometimes be more complex chains of prototypes present within an object. The thing to know is that while the structure (the number of references) might increase, in real-world applications you should only concerned with manipulating and extending the prototype of the Object constructor you are creating. This is typically done to avoid substantial changes to existing prototypes, which when done precariously can cause serious bugs.

Building objects: Factories, Functions and Classes

Since we have already gone over the basics of the Function factory, we should now discuss the two other types of JavaScript constructors: classes and factories.

Classes

In the last couple of years, the ambiguous syntax of class was added to the ECMAScript standards, allowing for a more syntax complete way of creating instances of Objects.

Here’s an example of a basic class within JavaScript

class Vegetable {
    // object constructor
    constructor(name, color, seedCount = 0){
        this.name = name;
        this.color = color;
        this.seeds = seedCount;
    }
    // class (object instance) methods
    hasSeeds(){
        return this.seeds > 0;
    }
}
const veggie = new Vegetable('Tomato', 'red', 10);
console.log(veggie, veggie.hasSeeds());
// Vegetable { name: 'Tomato', color: 'red', seeds: 10 }

Unlike a function constructor, a class has a more inclusive approach to creating object instances. Instead of interfacing directly with the prototype of an object, it provides a syntax alternative of specifying the methods directly within the scope. This also enables you to specify async functions as methods, which is useful when working with asynchronous code.

There’s an interesting caveat to class that most people fail to mention, which is that by design, the prototype of a class is not enumerable, meaning that when evaluated or logged it will return an empty object.

console.log(Vegetable.prototype); // Vegetable {}

This can be averted by using Object.getOwnPropertyNames to force an enumeration of the class prototype in question.

console.log(Object.getOwnPropertyNames(Vegetable.prototype));
// ['constructor', 'hasSeeds']

You can also print an existing object’s class methods with the following code:

console.log(Object.getOwnPropertyNames(
    Object.getPrototypeOf(veggie) 
);

Extending a class

Just like the classical definition, a JavaScript class generally inherits from a single ancestor.

class Tomato extends Vegetable {
    constructor(seedCount = 10, vineCount = 0){
        // call parent class and automatically apply to `this` context
        super('Tomato', 'red', seedCount);
        // custom methods
        this.vines = vineCount;
    }
    // define extra methods
    hasVines() {
        return this.vines > 0;
    }
}
// create a fairly bland virtual JS tomato
const tomato = new Tomato();
console.log(tomato); // Tomato { name: 'Tomato', color: 'red', seeds: 10, vines: 0 }
console.log(tomato.hasSeeds() ? 'has seeds' : 'no seeds'); // has seeds
console.log(tomato.hasVines() ? 'has vines' : 'no vines'); // no vines

This is done with the extends keyword, which tells the JavaScript engine to look for the parent constructor Vegetable and then use it when constructing the child class. The context of the parent function is then applied to the current with the use of the super call.

The cool part is that the constructor referenced by extends can be any valid function/class based constructor. This means you are able to extend almost any native Object type for your own needs, including Object, Array, String, Boolean and several others.

A quick example of a String being extended with a custom method:

// a custom class that retains all of a String's methods while
// adding custom methods
class CoolString extends String {
  constructor(value){
    super(value);
  }
  isCoolString(){
    return 'this is indeed, a cool string';
  }
}
const cs = new CoolString('   hello world   ');
console.log(cs.isCoolString()); // 'this is indeed, a cool string'
console.log(cs.trim()); // 'hello world'

Factories

There are some instances where the use of a function constructor or a class might not be best suited for the problem you’re tasked with solving. Instead of using a fork to shovel the driveway, it might better to use a factory constructor instead.

There are typically two ways to approach making a factory.

Object Literal approach:

const VegetableFactory = {
    build(name, color, seeds = 0) {
        // return vanilla object instance from the included model
        return Object.assign(
            Object.create(this.model), 
            {
                // define the default values
                name,  // alias for { name: name }
                color, // alias for { color: color }
                seeds, // alias for { seeds: seeds }
            }
        );
    },
    // define the object model's methods
    model: {
        hasSeeds() {
            return this.seeds > 0;
        }
    },
}
const veggie = VegetableFactory.build('Tomato', 'red', 10);
console.log(veggie, veggie.hasSeeds());
// { name: 'Tomato', color: 'red', seeds: 10 } true

Note that in lines 6-11, we used a ES6 (or ES2015) notation where

{name, color, seed}

is the same as:

{name: name, color:color, seeds: seed}

The VegetableFactory code above takes first creates a new object (using Object.create) with the specified prototype of this.model on line 5. It then assigns (using Object.assign) or copies the name, color, and seeds properties into this object. Note this newly returned object behaves just like the Vegetable class earlier.

Factory Function approach:

// define the model/methods
const VegetableModel = {
    hasSeeds() {
        return this.seeds > 0;
    }
};
function VegetableFactory(name, color, seeds = 0){
    // returns just like the Object literal except called via function
    return Object.assign(
        Object.create(VegetableModel), {
            name,
            color,
            seeds,
        }
    );
}
const veggie = VegetableFactory('Tomato', 'red', 10);
console.log(veggie, veggie.hasSeeds());
// { name: 'Tomato', color: 'red', seeds: 10 } true

This is very similar to the literal approach above, except it uses functions for the factory.


The Object.assign works as a way to bind the values to the returned instance instead of binding them to the context (this). The nice part is that Object.create returns the constructed model in such a way that the context is preserved, allowing the usage of this within any model methods.

Object.create actually takes two parameters, the first being a reference to the prototype object to construct from (which in this case is the Vegetable model), and the second being the enumerable properties of that object (which is useful when defining specific modifiers for an object’s properties). Since we are returning modifiable values in the object, we can use Object.assign as a shorthand method to return those properties.

Factories are believed to be safer by some since they are intended to avoid the prototype chain completely, resorting to creating very specific objects with specified methods.

Extending Factories

The best practice when creating factory functions is referencing the models in a new wrapper, since this avoids the usage of prototypes.

// the original Vegetable model
const VegetableModel = {
    hasSeeds() {
        return this.seeds > 0;
    }
};
// the extending Tomato model
const TomatoModel = {
    hasVines() {
        return this.vines > 0;
    }
};
function TomatoFactory(seeds = 10, vines = 0){
    return Object.assign(Object.create(
        // the TomatoModel overrides the VegetableModel
        Object.assign(TomatoModel, VegetableModel)), {
            name: 'Tomato',
            color: 'red',
            seeds,
            vines,
        }
    );
}
const tomato = TomatoFactory();
console.log(tomato); // { name: 'Tomato', color: 'red', seeds: 10, vines: 0 }
console.log(tomato.hasSeeds() ? 'has seeds' : 'no seeds'); // has seeds
console.log(tomato.hasVines() ? 'has vines' : 'no vines'); // no vines

The major difference is how lightweight factory objects are compared to class based objects. This is due to the prototype chain typically adding a substantial amount of overhead when dealing with large and complex types (often refereed to as the Gorilla-Banana problem).

The Gorilla-Banana problem is a problem with Object Oriented languages where one only wanted a Banana Object, but as a result included the Gorilla, and the entire Jungle too (all of the classes that comprise the Gorilla, the environment the Gorilla exists in, and everything comprising that - and so forth).

In this example we are also using Object.assign within the first parameter of the Object.create, which is essentially making it so the TomatoModel’s prototype will override any of the methods specified in the VegetableModel. In the Object.assign call, the first arguments take precedent over any of the ones following.

Since it only included the bare-minimum needed for an instance, it keeps models lean, which is incredibly useful for large collections with repetitive data.

Conclusion

Prototypes in JavaScript have been around since it’s inception, and while it’s usage is widespread: the majority of beginner programmers are overwhelmed by what it does and how it works. Hopefully this article was able to help give some needed clarity to prototypes, how they differ from classical inheritance, and how you can use them to your advantage.


© 2016-2022. All rights reserved.