Javascript: An Exploration of Prototypal Inheritance

Eureka! That’s the feeling I had when I finally saw how Object.create could help me in achieving the inheritance-oriented behavior I was after. I’d been reading lots about prototypal inheritance in JS and understood bits and pieces, but it finally started to click recently. Here are some thoughts and (intentionally simplified) explanations that will hopefully help you toward that eureka moment.

Note: This post is a long one and is catered to (intermediate) JS programmers with an understanding of objects, constructors, and a general comfort with the changing contexts of the this value. I couldn’t cover everything, but tried to include details that I felt were relevant to understanding inheritance in JavaScript.

Note: I’ll be using the Shapes example (from the Object.create page) on the Mozilla Developer Network as the basis for this tutorial.

If you have any suggestions, corrections, comments, or questions, feel free to tweet them at me: @mrjoelkemp.

The basics

// Constructor
function Shape() {
  this.x = 0;
  this.y = 0;
}

// A shape instance
var s = new Shape();

There are four “super-important” things to state right off the bat:

  1. s is an object and, by default, it has access to Shape.prototype (the prototype given to objects created with Shape constructor): really, just a partner object that’s “watching over” all Shape instances. You can think of an object’s prototype as a backup collection of attributes (variables/functions) that an instance looks to when it can’t find something on itself.
  2. This prototype is shared across all Shape instances. i.e., all instances have (indirect) access to it.
  3. When you call a function on an instance, the instance itself is checked for a definition of that function. If it wasn’t found, the prototype of that instance is checked for a definition.
  4. Wherever the definition of the function is found (on the instance or its prototype), the value of this is the instance that was used to call the function. So if we call a function on s, even if the definition is not directly on s but on s‘s prototype, the value of this, is the object s.

Let’s use the above ideas in an example. Let’s say we want to attach a function getPosition() to s. We could do it like this:

s.getPosition = function () {
  return [this.x, this.y];
}

This works. You can then just call s.getPosition() to get the resulting array.

But what if we created another Shape instance, s2; would it also have access to the getPosition() function?

No.

The getPosition function was created directly on the instance s. Hence, the function doesn’t exist on s2.

When you call s2.getPosition(), the following happens (#3 of the super-important things):

  1. The instance s2 is checked for a definition of getPosition.
  2. It doesn’t exist on s2.
  3. The prototype of s2 (the same shared, backup object for s) is checked for a definition of getPosition.
  4. It doesn’t exist on the prototype.
  5. The function definition can’t be found.

A simple (but not optimal) solution is to manually copy and paste the definition of getPosition onto s2 (and every instance that might want a getPosition function thereafter). This is bad because you’re unnecessarily duplicating code and you have a function defined on every instance which consumes more memory (if your program cares about that stuff).

There’s a better way.

Defining attributes on the prototype

We can achieve this sharing of the function by defining getPosition, not on every instance, but on the prototype of the constructor used to create every instance: Shape.

// Constructor
function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

var s = new Shape(),
    s2 = new Shape();

Since the prototype is shared across all Shape instances (#2 of the super-important things), both s and s2 have access to the getPosition function.

Calling s2.getPosition() has the following effect:

  1. The instance s2 is checked for a definition of getPosition
  2. It doesn’t exist on s2
  3. Check the prototype
  4. A definition of getPosition exists on the prototype
  5. getPosition is executed with this being s2.

Attaching attributes to the prototype is great for reuse. You reuse the same function definition for all instances of a given constructor.

When the prototype bites

Be careful when attaching objects (and arrays) to prototypes. All instances will share a reference to the attached object/array. If one instance manipulates the object (or array), all instances are affected.

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.types = ['round', 'flat'];

s = new Shape();
s2 = new Shape();

s.types.push('bumpy');

console.log(s.types); // ['round', 'flat', 'bumpy']
console.log(s2.types); // ['round', 'flat', 'bumpy']

When evaluating the line s.types.push('bumpy'), the instance s is checked for an array called types. It doesn’t exist on s, so the prototype is checked. The array, types, does exist on the prototype, so we add the element ‘bumpy’ to it.

Consequently, because s2 also shares the prototype, its understanding of the types array has changed indirectly.

A real-world example of when this bites is with Backbone.js. When you’re defining a View/Model/Collection, Backbone will add the properties that you pass to the extend function (ex: Backbone.View.extend({})) to the prototype of the entity that you’re defining.

This means that if you add an object or array when defining that entity, you’ll share that object/array and create the opportunity for one instance to ruin the party for every other instance. To avoid this, you’ll often see people wrap that object/array into a function that returns a new instance of that object/array.

Note: Backbone talks about this in the model defaults section:

Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Instead, define defaults as a function.

A different type of Shape

Let’s say we wanted to create a specific types of shape, like a Circle. It would be awesome to inherit all of the functionality of a Shape and still define custom functions on the prototype of the Circle constructor so that all circles can benefit.

function Shape() {
  this.x = 0;
  this.y = 0;
}

function Circle() {
  this.radius = 0;
}

So how do we say that a circle is a shape? There are a few ways:

1. Borrow the constructor and assign the prototype

When we create a circle, we want that instance to have a radius (coming from the Circle constructor), an x position, and a y position (coming from the Shape constructor).

If we just said c = new Circle(), then c would only have a radius. The Shape constructor did all of the initialization of x and y. We want that functionality. So let’s borrow it.

function Circle() {
  this.radius = 0;

  Shape.call(this);
}

The line Shape.call(this) calls the Shape constructor as a function and changes the value of this to point to the this value of this when the Circle constructor gets called. Say what?

Let’s create a new circle using the above constructor definition to see what’s going on:

c = new Circle();

This calls the Circle constructor, which firstly attaches a variable radius to c. Keep in mind, the this is c. We then call the Shape constructor, and set the value of this within the Shape constructor to the current value of this within the Circle constructor, again c. The Shape constructor attaches x and y to the current value of this; i.e., c now has an x and y set to zero.

As a side note, the order of where you put Shape.call(this) doesn’t matter in this example. If you wanted to override x and y after initialization ( to say that a circle starts at a different coordinate) then you would do so after the call to the Shape constructor.

The problem now is that a circle has the variables x, y, and radius, but it didn’t get anything from the Shape prototype. We need to set up the Circle constructor to reuse the Shape prototype as its own prototype – so that all circles get the benefits of being a shape.

One way to do this is to simply set the value of Circle.prototype to Shape.prototype.

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

function Circle() {
  this.radius = 0;
  Shape.call(this);
}

Circle.prototype = Shape.prototype;

var s = new Shape(),
    c = new Circle();

This works, but we’ll see why it isn’t optimal. The instance c now has access to the getPosition function because the Circle constructor shares its prototype with the Shape constructor.

What if we want to define a function getArea for circles? We’d attach it to the Circle constructor’s prototype so that it’s available to all circles.

Building on the previous code:

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

function Circle() {
  this.radius = 0;
  Shape.call(this);
}

Circle.prototype = Shape.prototype;

Circle.prototype.getArea = function () {
  return Math.PI * this.radius * this.radius;
};

var s = new Shape(),
    c = new Circle();

The issue is that since Circle and Shape share the same prototype, adding a new function on Circle.prototype is the same as adding it on Shape.prototype.

Whoopsie daisies.

A Shape instance doesn’t have a radius, only Circle instances have that. But now, all shapes have access to a getArea function – generating an error when called on shapes, but working properly when called on circles.

Setting the prototypes equal to each other won’t work for us.

2. The Circle prototype is a Shape instance

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

function Circle() {
  this.radius = 0;
}

Circle.prototype = new Shape();

var c = new Circle();

This method is pretty cool. We don’t have to borrow the constructor and Circles have an x and y, in addition to the getPosition function. How does it work?

Circle.prototype is now a Shape instance. This means that directly on c, there’s a radius (given to it by the Circle constructor). However, on c‘s prototype, there’s an x and y. Brace yourself, here it comes: on c‘s prototype’s prototype, there’s a definition of the getPosition function. It looks a bit like the following:

With that said, if you try to access c.x, it’ll be found on c‘s prototype.

The downside to this method is that if you wanted to override the value for x and y for Circles, you’d do so either in the Circle constructor or on Circle’s prototype.

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

function Circle() {
  this.radius = 0;
}

Circle.prototype = new Shape();
Circle.prototype.x = 5;
Circle.prototype.y = 10;

var c = new Circle();

console.log(c.getPosition()); // [5, 10]

A call to getPosition on c works as follows:

  1. the function is not found on c
  2. it’s not found on c‘s prototype (the shape instance)
  3. it’s found on the shape instance’s prototype (i.e., c‘s prototype’s prototype)
  4. The function gets called with its this value as c.
  5. Within the getPosition function’s definition, we look for x on this
  6. x is not found directly on c
  7. We check c‘s prototype (the shape instance) for x
  8. We find x on c‘s prototype
  9. We find y on c‘s prototype

Aside from the headache you now have due to the layers of look ups within the prototype chain, this method worked quite well.

An alternative way to achieve the same thing could be to use Object.create.

3. Borrow the constructor and use Object.create

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

function Circle() {
  this.radius = 0;

  Shape.call(this);
  this.x = 5;
  this.y = 10;
}

Circle.prototype = Object.create(Shape.prototype);

var c = new Circle();

console.log(c.getPosition()); // [5, 10]

One benefit of this approach is that x and y are attached directly to c – resulting in a faster lookup (if your program cares about that sort of thing) since you don’t have to look at the prototype.

Let’s look at the polyfill for Object.create:

Object.create = (function(){
  // Dummy constructor
  function F(){}
    
  return function(o){
    ...
    // Set the prototype of the dummy constructor to the
    // object that we are given, o
    F.prototype = o;
    // Return a new instance of this dummy constructor:
    // an empty object whose prototype is that which 
    // we were given, o
    return new F();
  };
})();

It basically does what Circle.prototype = new Shape(); did, except that Circle.prototype is now empty object (an instance of a dummy constructor, F) whose prototype is set to Shape.prototype.

Which method should you use?

It’s important to remember that if you have objects/arrays being attached in the Shape constructor, then all circles have the ability to modify that shared object/array. This bites you if you use the method where Circle.prototype is an instance of a Shape.

function Shape() {
  this.x = 0;
  this.y = 0;
  this.types = ['flat', 'round'];
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

function Circle() {
  this.radius = 0;
}

Circle.prototype = new Shape();

var c = new Circle(),
    c2 = new Circle();

c.types.push('bumpy');

console.log(c.types);  // ["flat", "round", "bumpy"]
console.log(c2.types); // ["flat", "round", "bumpy"]

To avoid this, you can borrow the Shape constructor and use Object.create so that every circle gets its own types array.

...
function Circle() {
  this.radius = 0;
  Shape.call(this);
}

Circle.prototype = Object.create(Shape.prototype);

var c = new Circle(),
    c2 = new Circle();

c.types.push('bumpy');

console.log(c.types);  // ["flat", "round", "bumpy"]
console.log(c2.types); // ["flat", "round"]

A more advanced example

Let’s take this discussion a step further (more along the lines of my eureka moment) and create a new type of Circle, a Sphere. A sphere is like a circle (which is like a shape), but has a different formula for finding the area.

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.getPosition = function () {
  return [this.x, this.y];
};

function Circle() {
  this.radius = 0;

  Shape.call(this);
  this.x = 5;
  this.y = 10;
}

Circle.prototype = Object.create(Shape.prototype);

Circle.prototype.getArea = function () {
  return Math.PI * this.radius * this.radius;
};

function Sphere() {
}

// TODO: Setup the prototype chain here

Sphere.prototype.getArea = function () {
  return 4 * Math.PI * this.radius * this.radius;
};

var sp = new Sphere();

Which method should we use to set up the prototype chain? Keep in mind, we don’t want to ruin the definition of getArea for circles. We just want spheres to have a different implementation.

We can’t borrow the constructor and assign the prototype (method #1). That would alter the definition of getArea for circles. We can, however, use either method of establishing the prototype chain: Object.create (with a borrowed Circle constructor) or setting the Sphere prototype as an instance of a Circle. Let’s see how it works.

...
function Circle() {
  this.radius = 0;

  Shape.call(this);
  this.x = 5;
  this.y = 10;
}

Circle.prototype = Object.create(Shape.prototype);

Circle.prototype.getArea = function () {
  return Math.PI * this.radius * this.radius;
};

function Sphere() {
  Circle.call(this);
}

Sphere.prototype = Object.create(Circle.prototype);

Sphere.prototype.getArea = function () {
  return 4 * Math.PI * this.radius * this.radius;
};

var sp = new Sphere();

A call to sp.getArea() works as follows:

  1. Look to sp for a definition of getArea
  2. No definition on sp
  3. Look at the Sphere prototype (the dummy object whose prototype is Circle.prototype)
  4. Find a definition of getArea on the dummy object, since we defined getArea on the Sphere’s prototype (line 22 of the code above) after the use of Object.create
  5. Evaluate the function getArea with sp as the value of this

Observe that Circle.prototype also has a definition of getArea. However, since Sphere.prototype has a definition of getArea, we never get to Circle.prototype’s implementation of getArea – successfully “overriding” that function (i.e., an override by way of having another function with the same name earlier in the lookup chain).

Where do we go from here?

The moral of this discussion is that you have to be aware of the consequences/limitations of the inheritance methods that you choose. If I were to select a trusty, go-to method, it might be Object.create with a borrowed constructor – as that seems the least prone to nasty runtime bugs.

If you have any questions or comments, feel free to tweet them at me: @mrjoelkemp.

Happy coding!


Discuss this article:

Hacker News
Reddit’s r/javascript

Comments

comments