In this final part of our series on JavaScript prototypes, we’re going to discuss using them to implement inheritance as it’s typically thought of in classical object-oriented design. Having said that, I once again suggest that you try to put aside what you may already know about how objects and inheritance work in other languages and treat the way JavaScript works as its OwnThing.
So, let’s get on with it.
The motivation
Let’s begin by laying out an example we’d like to work out. We already know how to write a constructor for a Person and put properties on the prototype so the same exact thing is shared among all instances.
function Person(name) { this.name = name; } Person.prototype.greeting = 'Hello. My name is'; Person.prototype.sayName = function() { console.log(this.greeting, this.name + '.'); }; var fuzz = new Person('Fuzz');
What we’d like to do next is create a way to instantiate Teacher objects. We’d like our Teacher to be a Person with the following additions:
- a
subject
property that contains the teacher’s area of expertise, and - a
saySubject
method that prints the teacher’s subject to the console.
In other words, a Teacher is a special kind of Person that has all the properties of a Person along with some additional ones.
Ideally, we want to make the Teacher definition rely on everything in the Person definition so that (a) we aren’t repeating ourselves, and (b) if we change or fix anything in the Person constructor, the changes are automatically propagated to Teachers as well. This requires two things:
- Making sure every instance of Teacher gets all the properties in a Person.
- Making sure every instance of Teacher gets access to all properties defined in Person’s
prototype
.
Inheriting properties on the object
To make sure that all the properties defined in the Person constructor also get applied to a Teacher, we can call the Person constructor inside the Teacher constructor, making sure to bind this
to the object being instantiated by Teacher. This is handily done with the call
or apply
methods:
function Teacher(name, subject) { Person.call(this, name); this.subject = subject; } var kittenstein = new Teacher('Kittenstein', "music"); console.log(kittenstein); // Teacher {name: "Kittenstein", subject: "music"}
Inheriting properties on the prototype
Making sure every Teacher instance gets access to everything in Person’s prototype
isn’t quite as straightforward. At first you might think the Teacher should have the same protoype as Person — until you realize that adding anything to Teacher’s prototype would also add it to Persons. Instead, what we want is to extend the prototype chain. In terms of a diagram, this is what we’re after:
This seems doable, but with a lot of gymnastics. Fortunately, there’s a standard JavaScript method that handles the hard part for us: the Object.create(<prototype>)
method lets you create a new object specifying what you want its __proto__
to be.
Thus, using Object.create
to make the new prototype
object for the Teacher class, we get:
function Teacher(name, subject) { Person.call(this, name); this.subject = subject; } // Override default Teacher.prototype: Teacher.prototype = Object.create(Person.prototype); // Attach specialized stuff to Teacher's prototype: Teacher.prototype.subjectText = 'I teach'; Teacher.prototype.saySubject = function () { console.log(this.subjectText, this.subject + '.'); };
Testing the above yields:
var kittenstein = new Teacher('Kittenstein', "music"); kittenstein.sayName(); // Hello. My name is Kittenstein. kittenstein.saySubject(); // I teach music.
One more thing
It looks like we might be done, but there’s one last subtlety we need to take care of. You might recall waaaay back in the first installment of this series we mentioned that prototypes have a property called constructor
that points back to the constructor it’s supposed to be associated with:
if (bar.prototype.constructor === bar) { console.log("That's cute."); } else { console.log('You lied.'); };
But if you now look at where Teacher.prototype.constructor
is pointing,
console.log(Teacher.prototype.constructor);
you’ll see that it’s pointing back at Person
, not Teacher.
Ugh.
So the last thing we need to do is assign Teacher.prototype.constructor
to point to Teacher
instead. A good place to do this is right after we attach the prototype to the constructor. This yields (finally):
function Teacher(name, subject) { Person.call(this, name); this.subject = subject; } Teacher.prototype = Object.create(Person.prototype); Teacher.prototype.constructor = Teacher; Teacher.prototype.subjectText = 'I teach'; Teacher.prototype.saySubject = function () { console.log(this.subjectText, this.subject + '.'); };
There has to be a better way
The above protocol has worked for millennia to implement classical object-oriented inheritance in JavaScript. One of its characteristics is that there’s nothing encapsulating the entirety of the object’s construction and behavior. The constructor, the attachment of properties to prototypes, and the overriding of prototypes to extend the prototype chain all happen outside of any formal structure. “With great power comes great responsibility.” But this also makes code less readable and harder to maintain. The verbosity doesn’t help either.
ES6 recognizes this and introduces syntactic sugar for doing what we’ve done here in a more concise and manageable way. But while ES6 classes abstract out a lot of the gyrations needed to implement classical OOP in JavaScript, it’s usually good to know what’s going on behind the scenes because it’s not uncommon for abstractions to get leaky.
Copyright © 2018 Mithat Konar. All rights reserved.