Prototypal Inheritance
Last year I wrote a post called "How to impress me in an interview", and in it, I mentioned that I run across a lot of candidates (the vast majority, actually) who don't really understand how prototypes work.
In a way, that's kind of an amazing testament to how flexible JavaScript is, and how user-friendly many of today's popular libraries are. I can't think of many other object-oriented languages where an engineer can be reasonably productive without being at least vaguely aware of classes.
But here's the thing: if you write JavaScript even semi-regularly, you really should understand how prototypal inheritance works. And that's why I'm writing this post.
Disclaimer
There will be some things in this article that I over-simplify a little bit.
My goal here is to help someone who's never used prototypal inheritance to become comfortable with it. In order to do that, I'll cut a few small corners here and there if I think it helps eliminate a bit of confusion.
Objects Inherit from Objects
If you've ever read anything about inheritance in JS, then you've almost certainly heard that objects inherit from other objects.
This is true, and once you already understand it, it's a good way to think about things. But my experience has been that this explanation alone isn't really sufficient.
So I'm going to break from tradition here and not actually try to explain how inheritance works right up front. Instead, we'll start with some simpler stuff that will hopefully provide a bit of context later on.
Function prototypes
Here's a fun fact: In JavaScript, all functions are also objects, which means that they can have properties. And as it so happens, they all have a property called prototype
, which is also an object.
function foo() {
}
typeof foo.prototype // 'object'
That's pretty simple, right? Any time you create a function, it will automatically have a property called prototype
, which will be initialized to an empty object.
Constructors
In JavaScript, there's really no difference between a "regular" function and a constructor function. They're actually all the same. But as a convention, functions that are meant to be used as constructors are generally capitalized.
By the way, if you don't know what I mean when I say "constructor", that's totally okay. We'll get there.
So let's say that we want to make a constructor function called Dog
, because explaining inheritance using animals is a time-honored tradition and I'm kind of a nostalgic dude.
function Dog() {
}
If I want to make an instance of Dog
, I use the new
keyword. That's really what I mean when I talk about constructors – I'm using the function to construct a new object. Any time you see the new
keyword, it means that the following function is being used as a constructor.
var fido = new Dog();
So now we have fido
, who's a Dog
. But he doesn't really do anything.
He's kind of like my dog, actually.
Methods
It's time to make our Dog
a little more dog-like. Bear with me here, because I'm going to explain this in just a minute.
function Dog() {
}
Dog.prototype.bark = function() {
console.log('woof!');
};
You should remember from earlier that all functions automatically get initialized with a prototype
object. In the example above, we tacked a function onto it called bark
.
Now let's make ourselves a new fido
.
function Dog() {
}
Dog.prototype.bark = function() {
console.log('woof!');
};
var fido = new Dog();
fido.bark(); // 'woof!'
I'm going to explain this in more detail a little later on. But the important thing to take away right now is that by placing bark
on Dog.prototype
, we made it available to all instances of Dog
.
Don't worry yet about how this works. Just keep in mind that it works.
Differential Inheritance
JavaScript uses an inheritance model called "differential inheritance". What that means is that methods aren't copied from parent to child. Instead, children have an "invisible link" back to their parent object.
For example, fido
doesn't actually have its own method called bark()
(in other words, fido.hasOwnProperty('bark') === false
).
What actually happens when I write fido.bark()
is this:
- The JS engine looks for a property called
bark
on ourfido
object. - It doesn't find one, so it looks "up the prototype chain" to
fido
's parent, which isDog.prototype
. - It finds
Dog.prototype.bark
, and calls it withthis
bound tofido
.
That part is really important, so I'm going to repeat it:
There's really no such property as fido.bark
. It doesn't exist. Instead, fido
has access to the bark()
method on Dog.prototype
because it's an instance of Dog
. This is the "invisible link" I mentioned. More commonly, it's referred to as the "prototype chain".
Object.create()
Okay. We talked a little bit about differential inheritance and the prototype chain. Now it's time to put that into action.
Since ES5, JavaScript has had a cool little function called Object.create()
.
Here's how it works.
var parent = {
foo: function() {
console.log('bar');
}
};
var child = Object.create( parent );
child.hasOwnProperty('foo'); // false
child.foo(); // 'bar'
So what is it doing?
Essentially, it creates a new, empty object that has parent
in its prototype chain. That means that even though child
doesn't have its own foo()
method, it has access to the foo()
method from parent
.
Try It
We've covered a lot of ground so far, and we still have a bit more to go. To make sure everything we learned so far sinks in, I'd encourage you to open up your dev tools and try a quick little example.
Create a constructor function called Car
. Add a drive()
method to its prototype that just logs to the console. Now create an instance of Car
and call drive()
.
I really mean it. You should take 30 seconds and go do it. I'll wait here. And no cheating...
Good, you're back. Hopefully you wrote something that looks like this.
function Car() {
}
Car.prototype.drive = function() {
console.log('vroom');
};
var benz = new Car();
benz.drive(); // vroom
If not, consider re-reading some of the earlier sections. It's important that you're somewhat comfortable with that stuff before you move on.
Putting It All Together
It's time to look at a (slightly) more real-world example that takes everything in this post and puts it all together.
Let's start by creating a Rectangle
constructor.
function Rectangle( width, height ) {
this.width = width;
this.height = height;
}
At this point, we'll take a tiny detour to talk about this
.
When a function is used as a constructor, this
refers to the new object that you're creating. So in our Rectangle
constructor, we're taking width
and height
as arguments, and assigning those values to the width
and height
properties of our new Rectangle
instance.
var rect = new Rectangle( 3, 4 );
rect.width; // 3
rect.height; // 4
Now it's time to give our Rectangle
a method. Let's call it area
.
Rectangle.prototype.area = function() {
return this.width * this.height;
};
There's that this
keyword again. Just like in the constructor, this
inside of a method refers to the instance.
var rect = new Rectangle( 3, 4 );
rect.area(); // 12
Subclassing
What if we want to make a new class of object that inherits from Rectangle
?
Let's say we need a class called Square
. Hopefully you remember from elementary school that a square is just a specific type of rectangle.
We'll start by creating its constructor.
function Square( length ) {
this.width = this.height = length;
}
So, that's all well and good, but how do we make Square
inherit from Rectangle
? It's all about setting up the prototype chain.
If you remember from earlier, we can use Object.create()
to create an empty object that inherits from another object. In the case of Square
, that means all we need to do is this:
Square.prototype = Object.create( Rectangle.prototype );
All instances of Square
will automatically have Square.prototype
in their prototype chain, and because Square.prototype
has Rectangle.prototype
in its prototype chain, every Square
will have access to the methods of Rectangle
.
In other words, we can do this:
var square = new Square( 4 );
square.area(); // 16
We can also add new methods that are specific to Square
. Remember, Square.prototype
is just an empty object right now (albeit with a link back to Rectangle.prototype
).
Square.prototype.diagonal = function() {
return Math.sqrt( this.area() * 2 );
};
Time travel
One of the really cool (and potentially dangerous) things about inheritance in JavaScript is that you can modify or extend the capabilities of a class after you've defined it.
Because JavaScript will look up the prototype when trying to access properties on an object, to you can alter your classes at runtime.
Here's an example (for illustrative purposes only. Don't ever do this):
var arr = [ 1, 2, 3, 4, 5 ];
Array.prototype.shuffle = function() {
return this.sort(function() {
return Math.round( Math.random() * 2 ) - 1;
});
};
arr.shuffle(); // [ 3, 1, 4, 5, 2 ]
The important thing to notice in this example is that arr
was created before Array.prototype.shuffle
existed. But because property lookups are done at runtime, our array got access to the new method anyway. It's like we went back in time and gave Array
a (stupid) new method.
To get a sense of how powerful (and potentially dangerous) this is, go open the JS console on a page like Facebook, paste in the following code, and watch the log as you click around:
Array.prototype.push = function() {
throw new Error('lolnope');
};
If you live in the Boston area, love JavaScript, and want to work on some really exciting problems at Starry, shoot me an email. I'm hiring.