Class-Based Inheritance Implemented in JavaScript
© 26 March, 2013, Martin Rinehart
Prerequisite: solid JavaScript object programming (see OP and OOP, above).
This page leans heavily on the JavaScript convention that a constructor starts with a capital letter, and nothing else does. The function Duck
is the constructor for all duck
objects. That's too subtle for our "make it readable!" bias, but that's JavaScript.
Note to self: this page is just as bad as everything else you've read re the "magic" commonly recommended. We need some kind of machine that every step of the way shows what happens, visible and otherwise.
Most pages/posts/articles on inheritance and JavaScript show the method we'll explain here. Before we get started, two caveats. First, we don't recommend this method as it is quite unreadable (unless you have read this page, carefully). Second, we don't even recommend the use of inheritance in JavaScript. It is almost always a design failure committed by a JavaScript beginner coming from
JavaScript lets you do object programming (not just object-oriented programming). With object programming there is almost never a need for inheritance. On this site you might start with OP and OOP and continue with OP Examples. (Both are on the menu above.) With object programming skills, you won't need the information in this page (unless you need to read the code you've "inherited" from someone less skilled).
Prototypal Inheritance, Demonstrated
Let's create a simple Animal
class from which other, more specific animals will inherit, such as Duck
s. Each Animal
will have a message specific to the class, and will share the talk()
method (talk()
calls co()
, co()
calls console.out()
, if available, otherwise alert()
.) talk()
is found in the base Animal
class.
This is the base class:
function Animal() { this.msg = "Animal: Nothing to say."; } Animal.prototype.talk = function () { co( this.msg ); } // test: new Animal().talk();
When you run that much, it will report "Animal: Nothing to say."
. The Animal
is a base class. It expects you to write a class that inherits the talk()
method but provides its own msg
. A class like this:
function Duck() { this.msg = 'Duck: Quack. Quack. Quack.'; } Duck.prototype = new Animal(); // test: new Duck().talk();
Now you have a Duck
class. Instances of Duck
have their own msg
and inherit the talk()
method from the base Animal
class.
This could hardly be easier. Unless, of course, you actually wanted some code that a mere mortal could read. There's lots of magic in this line:
Duck.prototype = new Animal();
Before we go on, there are lots of other ways to arrange inheritance. This is pure prototypal inheritance and we doubt that one JavaScripter in ten actually understands it. If you want to know how this magic works, the next section is for you.
Prototypal Inheritance, Explained
You've probably read that JavaScript uses prototypal inheritance. This is sloppy bordering on dead wrong. JavaScript can use prototypal inheritance. We avoid it because the code is unreadable. Cheryl, who will have to maintain our code after we've moved on, cannot read it. If your maintainer cannot read your code, you've written bad code. (Cheryl? She lives here.)
The Prototype Chain
You've probably read about the prototype chain. Let's have a quick review. If an object property is specified (like the talk()
method property of a duck
object) and the object doesn't have such a property, JavaScript looks for it in the object's prototype. (Note that the object's prototype
is not a property named prototype
. More on that coming.).
The preceding paragraph can be read, and is processed, recursively. The object's prototype is an object. So if the property is not found in the object's prototype, it is looked for in the prototype of the object's prototype. This recurses back to the granddaddy of all prototypes, Object.prototype
. (In practice, this recursion is rarely more than two or three steps long.)
And that tells us what we want: the duck
doesn't have a talk()
method. The duck
's prototype
doesn't have one, either. But the prototype of the prototype of the noisy canard knows how to talk. Now, how did that simple line achieve this?
The new
Operator
Unfortunately, for those of us who are big fans of readable code, the JavaScrpt new
operator does lots of magic (things you can't see). The one's we care about are these.
First, new
creates an empty object. Inside the constructor it will be called this
. Second, it attaches an implicit property (a property that you cannot read or write) (referred to as "[[property]]" in the ECMAScript specs) to the this
object. That property is a reference to the real prototype, which is the constructor's prototype
property. There is also an explicit property (regular property that you can read and write) named constructor
attached to the constructor function's prototype
property. Asking for this.constructor
gets a reference to the constructor function. (There is no constructor
property for this
. Therefore, JavaScript looks in the prototype for this
which is this.constructor.prototype
, where it finds the constructor
property.)
If the new
operator is calling the Duck
constructor, it's as if there were a line of code that said:
this.constructor = Duck;
Note that Duck
, is the function itself. No parentheses. (Parentheses would call the function, returning the function's result.)
More exactly, there is no constructor
property for the this
object. So the property is looked for in this
's prototype, which is "[[prototype]]" (the one you can't read) that refers to Duck.prototype
, a regular property that you can read and write.
The last bit of magic that new
performs, when your constructor has run to completion, is to reach inside the constructor, grab this
and yank it right out of the function, passing it along to the left.
Assignment to the Prototype
If you are unclear on any of the steps of the new
operator, reread the above. Slowly would be smart.
Now you see what happens, mostly. The one thing that we haven't covered is another bit of magic. When JavaScript finds a duck
trying to talk()
(or when it sees any property that is not part of the duck
object) it looks to the duck
's prototype, which is not a property named prototype
. It is the implicit property that the specs call "[[prototype]]". (Those brackets are not the code brackets that delimit arrays. They are special syntax in the specs that say, "You can't read or write this property.")
So now you know that every object has a prototype and it is available to your code, for a Duck
, as duck.constructor.prototype
and it is available to your JavaScipt engines as the duck's implicit "[[prototype]]" property.
So now let's work this through, one step at a time:
// magic line: Duck.prototype = new Animal();
You know that Duck
is the constructor for each duck
object. After the new
operator it will be available (by lookup in the prototype) as the constructor
property:
duck.constructor // reference to Duck
We know that an object's constructor.prototype
is the object's [[prototype]]
. So we are assigning to the prototype of our duck
object, the one that JavaScript will be looking to for properties it needs to find.
We know that the new
operator, when we call new Animal()
, has created a this
object and that:
this.constructor === Animal; // by prototype lookup
Since the this
object's "[[prototype]]" is Animal.prototype
we have assigned two things: the anonymous Animal
's msg
property and the anonymous Animal
's prototype, which is Animal.prototype
. The latter has a single method, talk()
. Since the duck
has been given its own msg
property, the new msg
inherited from the Animal
is overridden.
However, the talk()
method in the Animal.prototype
is the only one available in the prototype chain. Follow slowly:
duck
, an object created by calling new Duck()
, received a msg
from the Duck
constructor. It also received a new Animal()
in its own constructor. The new Animal()
has a msg
but JavaScript will ignore it. The duck.msg
comes at the very head of the prototype chain. But what is important is the talk()
function in the Animal
's prototype. This is the prototype of the duck
's prototype so it will provide the talk()
method since it is the first (and only) one in the prototype chain.
To repeat, there is no direct duck.talk()
method, so JavaScript will look for one in the duck
's prototype. The duck
's prototype is Duck.prototype
. That also has no talk()
method so Duck.prototype
's prototype will be searched. That is Animal.prototype
where the talk()
method is found.
Alternatives to Prototypal Inheritance
Is there a simpler way to get the result we want? The answer is yes. But to find the answers to questions like that, you have to focus closely on defining the result you want. (Road maps are more helpful if you have a destination.)
In our example, we inherited from the base class. This was not the goal, however. We wanted to use the method talk()
for all of our Duck
s (and, one assumes, for Dog
s and Donkey
s, too). Accessing the base class method is our goal. Let's try to get this job done.
Method 1
We could have just made the change highlighted in this listing:
function Duck() {
this.msg = 'Duck: Quack. Quack. Quack.';
}
Duck.prototype = Animal.prototype;
// test:
new Duck().talk();
Instead of creating a new object and using it as a prototype, we just make another reference to the prototype object. Now we have both Animal.prototype
and Duck.prototype
pointing to the same object. It has one method: talk()
. This would work just as well if we were sharing a hundred methods, too. In addition to its simplicity, it also avoids the lookup from the Duck
to the Animal
class, saving time (probably negligible, but these savings mount up) on each direct access from Duck
to the base class.
Disadvantage? We don't copy the msg
data property into the Duck
prototype. In our example that's no problem. We didn't use the msg
in the prototype. We used the duck
's own msg
. But if we had a large collection of data properties in the base class, this would be an issue.
We wouldn't care to argue against someone who said that this isn't really inheritance. But that's why we pointed out that our goal was to share a method, not to use a particular technique for sharing methods.
Method Two
Here we show a more radical departure. We've rewritten the library routine, co()
and the object instance method talk()
as a single library routine. It takes, as arguments, the message and the object calling the routine. (That is commonly needed to convert an object routine into a library routine. In this case, we have no immediate use for the calling object.)
function talk( talker, msg ) { if ( console && console.out ) { console.out( msg ); } else { alert( msg ); } } function Animal() { this.msg = "Animal: Nothing to say."; } Animal.prototype.talk = function () { talk( this, this.msg ); }
The advantage of the library routine is the absense of a this
magic reference. (In JavaScript, this
is a common source of trouble.) This is a particularly helpful technique in reverse: when you are converting code from library-based functions to object-based methods.
Method Three
Like Method Two, only this time convert the Duck
prototype to call the library routine directly, instead of just borrowing the base class prototype. The obvious disadvantage is that you repeat the calling code in both classes. (Possibly, when you get to adding dog
s and donkey
s, and a hundred others, this may be a serious isse.) Whether this helps or hurts depends on future directions. Will all the other fish and fowl on the farm share the same method, or will you need to customize the method a bit for the different types of things that talk()
. If your destination is separate routines, starting with separate routines may make sense.
Method Four
A "mixin" is now a JavaScript classic for sharing functionality without inheritance. You'll need to look this one up when you are next tempted to fall back on an inheritance-based design.
More Methods
The beauty of object programming is that it lets you have so much freedom in structuring the solution you want. We could place the talk()
in the Duck
's prototype and let all the other animals access it there. (In this example that would be a bit strange. Did we want the ducks to say "Moo!"? We have seen examples in practice where this makes sense, however.) Really, the only limit is your creativity, which is why so many JavaScripters are in love with the language.
Feedback: MartinRinehart at gmail dot com
# # #