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
// Constructorfunction Shape() { this.x = 0; this.y = 0;}// A shape instancevar s = new Shape();
There are four “super-important” things to state right off the bat:
- 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.
- This prototype is shared across all
Shape
instances. i.e., all instances have (indirect) access to it. - 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.
- 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 , even if the definition is not directly on but on ‘s prototype, the value of this, is the object
Let’s use the above ideas in an example. Let’s say we want to attach a function getPosition()
to . 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 . Hence, the function doesn’t exist on s2
When you call s2.getPosition()
, the following happens (#3 of the super-important things):
- The instance s2 is checked for a definition of
getPosition
- It doesn’t exist on s2
- The prototype of s2 (the same shared, backup object for ) is checked for a definition of
getPosition
- It doesn’t exist on the prototype.
- 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
// Constructorfunction 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 and s2 have access to the getPosition
function.
Calling s2.getPosition()
has the following effect:
- The instance s2 is checked for a definition of
getPosition
- It doesn’t exist on s2
- Check the prototype
- A definition of
getPosition
exists on the prototype getPosition
is executed with this beings2
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 is checked for an array called types
. It doesn’t exist on , 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 position, and a position (coming from the Shape constructor).
If we just said c = new Circle()
, then would only have a radius. The Shape constructor did all of the initialization of and . 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 . Keep in mind, the this is . 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 . The Shape constructor attaches and to the current value of this; i.e., now has an and 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 and 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 , , 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 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 and , in addition to the getPosition
function. How does it work?
Circle.prototype
is now a Shape instance. This means that directly on , there’s a radius (given to it by the Circle constructor). However, on ‘s prototype, there’s an and . Brace yourself, here it comes: on ‘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 ‘s prototype.
The downside to this method is that if you wanted to override the value for and 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 works as follows:
- the function is not found on
- it’s not found on ‘s prototype (the shape instance)
- it’s found on the shape instance’s prototype (i.e., ‘s prototype’s prototype)
- The function gets called with its this value as
- Within the
getPosition
function’s definition, we look for on this - is not found directly on
- We check ‘s prototype (the shape instance) for
- We find on ‘s prototype
- We find on ‘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 and are attached directly to – 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, ) 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 hereSphere.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:
- Look to sp for a definition of
getArea
- No definition on sp
- Look at the Sphere prototype (the dummy object whose prototype is Circle.prototype)
- Find a definition of
getArea
on the dummy object, since we definedgetArea
on the Sphere’s prototype (line 22 of the code above) after the use ofObject.create
- 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: