Javascript is weird.
Prototypical inheritance is really powerful, but it’s quite a bit to wrap your mind around if you’re new to it. My first language was Java, and I’m comfortable with the “traditional” OOP paradigm. In this post, I’ll build a traditional Superclass/Subclass relationship in Javascript in an attempt to untangle the weirdness that is Javascript’s prototypical inheritance model.
All of the source code is available in a repl.it.
Part 1: Superclass Constructor
Let’s brew a cup of tea.
//Superclass constructor
var Tea = function(options) {
this.color = options.color || "herbal";
this.caffeine = options.caffeine || 0;
this.tannins = options.tannins || 0;
this.minsSteeped = 0;
}
Here, we’ve created a constructor for some tea. This tea can’t really do anything, but it holds some useful information on whatever tea object we create in the future. I can make a cup of herbal tea by calling this with the new
keyword.
var myHerbalTea = new Tea({});
Part 2: Superclass Prototype
Now that we have an idea of how prototypes work, we can go ahead and construct a prototype that will be applied to instances of Tea
.
Tea.prototype = {
steep : function(cup) {
//caffeine
this.caffeine = Math.max(0, this.caffeine - 1);
if (this.caffeine) {
cup.caffeine ++;
}
//tannins
cup.tannins += this.tannins * this.minsSteeped;
//color
cup.color = this.color;
//minsSteeped
this.minsSteeped ++;
}
};
Now we create an object with a single method, steep
, and assign that to be the prototype of Tea
. We usually want to put methods on the prototype, so that there’s only one copy of the function shared among all instances (this saves us a lot of memory).
After calling var myHerbalTea = new Tea({})
, we’ll have the following inheritance structure:
Notice that the Tea
function is not part of the prototype chain. It is simply a constructor function used to add the member variables (color
, etc) to myHerbalTea
. However, because Tea.prototype
exists, it is chained to our instance of the Tea
class.
Part 3: Subclass
Herbal tea is boring. Let’s make something to wake us up!
//Subclass constructor
var BlackTea = function(){
//Put instance properties from the superclass constructor on the subclass instance
Tea.call(this, {
color : "Black",
caffeine : 10,
tannins : 3
});
this.hasMilk = false;
}
Here’s where the Javascript magic begins. And by magic I mean less of the Harry Potter and more of the Goat Sacrifice.
Let’s make some black tea:
var firstCup = new BlackTea();
This is another constructor function, composed of two main parts. First, we call the Tea()
function, binding to the current instance. The call to Tea
will have firstCup
bound to this
, which will apply all of the instance variables (color
, etc), to firstCup
. This is kind of like calling super()
in a constructor in Java.
Here’s the relevant code. Everything that refers to firstCup when we create it is highlighted red.
var Tea = function(options) {
this.color = options.color || "herbal";
this.caffeine = options.caffeine || 0;
this.tannins = options.tannins || 0;
this.minsSteeped = 0;
}
// ...
var BlackTea = function(){
Tea.call(this, {
color : "Black",
caffeine : 10,
tannins : 3
});
this.hasMilk = false;
}
var firstCup = new BlackTea();
// ...
Ok, pop quiz. what is currently the prototype of firstCup
?
Answer: undefined
.
If you thought it was Tea
, remember that Tea
is just the constructor and is never really a “thing”.
If you thought it was Tea.prototype
(I totally did), know that even though we do Tea.call(...)
, we haven’t bound BlackTea
‘s prototype yet, so firstCup.__proto__
is nothing.
Let’s set up the prototype and make a second cup.
Part 4: Subclass Prototype
Remember that the prototype of a class is the object that will be assigned to the __proto__
property of any instances created of that class. So in order to create a second cup of tea, we need to first change BlackTea.prototype
.
BlackTea.prototype = Object.create(Tea.prototype);
var secondCup = new BlackTea();
It wasn’t obvious to me at first why we don’t bind BlackTea.prototype
directly to Tea.prototype
. Instead, we create an object whose prototype is Tea.prototype
, and then set BlackTea.prototype
to that new, empty object. Our secondCup
object can still see the steep(cup)
function, but it checks an empty object on its way up the prototype chain. Hmm…
But say we want to be able to add milk to our black tea. (Adding milk to green or herbal tea is icky and we shouldn’t allow our users to do that). If I modify BlackTea.prototype
to be able to add milk:
BlackTea.prototype.addMilk = function(cup) {
//adding milk makes the cup of tea less harsh!
cup.tannins = Math.floor(cup.tannins / 2);
this.hasMilk = true;
}
Remember that BlackTea.prototype
is not Tea.prototype
! It’s an object whose prototype is Tea.prototype
. This means that addMilk is only available to instances of BlackTea, not any kind of tea.
Our prototype chain now looks like
Yay!
Part 5: Actually Make Tea
//ok, let's make some tea
var myTea = new BlackTea();
var myCup = {
color : "clear",
caffeine : 0,
tannins : 0
};
//steep for three minutes
myTea.steep(myCup);
myTea.steep(myCup);
myTea.steep(myCup);
//add some milk
myTea.addMilk(myCup);
//drink up
console.log(myCup, myTea);
This produces:
myCup is { color: ‘Black’, caffeine: 3, tannins: 4 }
myTea is { color: ‘Black’, caffeine: 7, tannins: 3, minsSteeped: 3, hasMilk: true }
Part 6: Recap
- Javascript prototypal inheritance is super weird
- Use the new keyword to treat a function as a constructor
- The prototype of that function will become the __proto__ of the object, which is where the object looks for methods that aren’t declared locally.
- Then you can call that constructor (like Java’s
super()
) usingClass.call(object)
- Explicitly set prototypes to an object with the prototype you want to create
- e.g. myObject.prototype = Object.create(Constructor.prototype)
I hope this blog post cleared up some of the mystery around prototypes and inheritance!
From Manhattan,
–Erty Seidohl
P.S. Thanks to Ryan McVerry and Julia Evans for proofreading! Also Michael Mulley for helping me understand the WTF-ness of .prototype
not actually returning anything on an object.
Leave a Reply