2010-12-21
JavaScript Prototypes and Inheritance
Someone on Hacker News asked for a concise description of "prototype" in JavaScript. I gave it a go, but the topic closed before I could post. So I saved it here for posterity.
And then I blew it out and made it considerable less concise, which might or might not be for the best, but you can be the judge of that.
Prototype-based inheritance in JavaScript can be a little bit strange to people accustomed to class-based inheritance, but it really doesn't need to be difficult to grok. Like so many things, once you "get it", it seems easy enough.
Before we begin, let's do a small bit of terminology. In JavaScript, an object's "constructor" is itself an object. For example, you might have seen something like this:
function Kitten() { this.fuzzy = true; }
var k = new Kitten(); // "Kitten" is k's "constructor"
But if you look, you see that Kitten
is an object, right? It's an
instanceof
a Function
, in particular:
js> Kitten instanceof Function
true
Since Kitten
is an object, it means Kitten
can have properties, such
as Kitten.prototype
.
Just hold onto that for a moment; we'll come back to it.
Here's a tricky one: an object's prototype is an implicit reference to
the value of object's constructor's prototype
property. That is, the
object's prototype is the object pointed to by its constructor's
prototype
property.
Even saying it two different ways doesn't really seem to take the edge off, does it?
Let's look at the Kitten
example, above. In it, k
's constructor is
Kitten
. Therefore, k
's constructor's prototype
property is
Kitten.prototype
. Therefore, by the above definition, k
's prototype
is Kitten.prototype
.
Tersely, and only slightly incorrectly: k
is-a Kitten
, so k
's prototype
is Kitten.prototype
.
(The "object's prototype" is different than the "object's prototype
property"! I know! Remember, the object's prototype is its
constructor's prototype
property! This is a zany terminology thing
that can really get confusing.)
So when you construct an object, that object gets an implicit reference to the value of the constructor's "prototype" property, which is an object:
js> Kitten.prototype
[object Object]
Now, by "implicit reference", I mean that it's an internal thing, not
some property on your object. In the Kitten
example, k.prototype
is
not the same as Kitten
or Kitten.prototype
. The implicit
reference is hidden away. (It can, however, be gotten with
Object.getPrototypeOf()
.)
Finally, Here's The Meat: If a property cannot be found in an object, it is searched for in that object's prototype.
That's basically it.
All the rest of it is neato emergent behavior.
Simple example:
// here's the constructor:
function Weasal() { } // Weasel is to Weasal as Kernel is to KERNAL
// set up the prototype object to have some values:
Weasal.prototype = { x: 10, y: 20 };
// or you could do this:
Weasal.prototype.z = 30;
// make a new Weasal object
// (this object gets an implicit reference to Weasal.prototype object)
var frank = new Weasal();
frank.x == 10; // TRUE -- from Weasal's prototype
frank.z == 30; // TRUE -- from Weasal's prototype
frank.hasOwnProperty('x'); // FALSE -- it's in Weasal's prototype object
// now check this out:
frank.x = 30; // assign a value to frank's x property
frank.x == 30; // TRUE -- but now we're looking at frank's x!
frank.hasOwnProperty('x'); // TRUE -- Weasal's prototype x is hidden
// and then:
delete frank.x; // blow away frank's x property
frank.x == 10; // TRUE -- it's looking at Weasal's prototype again
Here's an example involving making "methods" on an object:
function Shark() { }; // constructor
Shark.prototype.fireFrickinLaserBeam = function() { doit(); };
fred = new Shark();
// fred doesn't have a fireFrickinLaserBeam property, so it is looked for
// in its prototype object (Shark.prototype) and runs from there:
fred.fireFrickinLaserBeam();
And here's a more complex example involving inheritance:
function Legume() { }
Legume.prototype.health = 20;
function Bean() { }
Bean.prototype = new Legume(); // inheritance! See below for discussion
Bean.prototype.audio = true;
var b = new Bean();
b.goats = 3490;
Take a look at that code, and see how it compares to the following diagram, which shows the objects and their properties:
Four objects and their properties. The dotted lines represent the implicit connection to the object's prototype (put obtusely, the dotted lines represent the implicit connection to the object referred to by the object's constructor's prototype property.
At this point, b
only has the goats
property ... so you if reference
b.audio
, it looks for it in its prototype object
(Bean.prototype
)and gets it there, just like in the previous examples.
This is a specific case of our original general rule: if an object doesn't have the property you're looking for, the property is looked for in that object's prototype object.
Now for the magic: you might have noticed that the word "object" appears at the beginning and end of that rule... the rule applies to both the object and the object's prototype because the object's prototype is an object.
This means that if the property isn't found in the prototype object, it is looked for in the prototype object's prototype! And turtles all the way down the "prototype chain".
To run the example, then, when we try to evaluate b.health
:
-
Is
health
inb
? No. So... -
Since
b
's prototype isBean.prototype
, ishealth
inBean.prototype
? No. So... -
Since
Bean.prototype
's prototype isLegume.prototype
, ishealth
inLegume.prototype
? Yes! Bing! We got it!
In this way, you can build an inheritance tree by chaining prototypes to
other objects, just like we chained Bean.prototype
up to a Legume
object. We could make a further subclass Pinto
, and set its
prototype
to a Bean
object, for example.
Setting the constructor
property
One thing that I didn't mention is that prototype
properties of
Function
s have a constructor
property. This property references the
function itself, i.e. the constructor. For example:
function Beet() { } // constructor
Beet.prototype.constructor == Beet; // TRUE
// now we can conveniently get the constructor of an object through the
// established magic of the prototype chain:
var b = new Beet();
b.constructor == Beet; // TRUE, from Beet.prototype
However, this gets hosed when you do inheritance. Here's the breakage, just so you understand it:
function Goat() { } // constructor
function UltraGoat() { } // constructor
UltraGoat.prototype = new Goat(); // inherit from Goat
// Goat.prototype is the built-in Function prototype property, which has a
// constructor property referring to "Goat":
Goat.prototype.hasOwnProperty('constructor'); // TRUE
Goat.prototype.constructor == Goat; // TRUE
// HOWEVER, UltraGoat's prototype is just a normal object created with new. It
// doesn't even have a constructor property in its prototype property:
UltraGoat.prototype.hasOwnProperty('constructor'); // FALSE (trouble brewing!)
// and so, when you try to get its constructor, it goes up the prototype
// chain to Goat.prototype!
UltraGoat.prototype.constructor == UltraGoat; // FALSE, Aghh!!
UltraGoat.prototype.constructor == Goat; // TRUE, wrong!!
Fortunately there is a quick fix. Now, if you never reference your
Ultra-Goat's constructor
, this won't be a problem. But nevertheless
people commonly patch up the constructor
property when inheriting,
like so:
function Goat() { } // constructor
function UltraGoat() { } // constructor
UltraGoat.prototype = new Goat(); // inherit from Goat
UltraGoat.prototype.constructor = UltraGoat; // fix the constructor property
// make some goats:
var goat = new Goat();
var ultraGoat = new UltraGoat();
// and now this will work properly:
goat.constructor == Goat; // TRUE, as expected
ultraGoat.constructor == Goat; // FALSE, as expected
ultraGoat.constructor == UltraGoat; // TRUE, as expected
There's more fun to be had with prototypes, but you're a lot of the way there, now.