JavaScript inheritance is different from other object-oriented languages due to the prototypal nature of the language. Understanding this prototype behavior will help prevent common pitfalls and sneaky issues that may unexpectedly arise. This article covers inheritance and can be especially useful for new developers that are just starting to work with the Javascript language.
We'll be diving right in, but if you'd like more background information (and also more examples) on prototypes, see Mozilla's awesome documentation on the subject.
Let's start off with a generic Robot
object with the following methods:
1function Robot() {}23Robot.prototype.turnOn = function () {};4Robot.prototype.turnOff = function () {};5Robot.prototype.isPoweredOn = function () {};6Robot.prototype.captureImage = function () {};
So we have our "methods" (function defined on a class) attached to our prototype. The Robot
can capture images, turn on/off, and report it's state.
Eventually, we'll want to specialize our Robot
into something more specific.
1function SpeedCamera() {}23SpeedCamera.prototype.calculateObjectSpeed = function (x, y, z) {};45const cameraRobot = new SpeedCamera();
The SpeedCamera
instance might have a new method (calculateObjectSpeed
), but it will need all of the previous methods inherited from the base Robot
class. After all, it should be able to turn on/off, report its state, and surely capture images.
Let's consider the following ways to implement inheritance:
1/** Option 1: Copy the functionality on the prototype */2SpeedCamera.prototype = {3 turnOn: Robot.prototype.turnOn,4 turnOff: Robot.prototype.turnOff,5 isPoweredOn: Robot.prototype.isPoweredOn,6 captureImage: Robot.prototype.captureImage,7};89/** Option 2: Set the two prototypes equal to each other */10SpeedCamera.prototype = Robot.prototype;1112/** Option 3: Set the SpeedCamera prototype to a new instance of Robot */13SpeedCamera.prototype = new Robot();
The first option isn't very good. The properties might be available on the object, but they are simply copied over from the Robot
prototype. Implementing this is a little daunting since we would have to copy those same properties over and over again for every new instance of SpeedCamera
. It would also seem like a violation of DRY. Additionally, this approach would introduce a higher memory overhead. Lastly, this method also fails the instanceof
check, so we know our SpeedCamera
instantiation (cameraRobot
) is not a true Robot
.
1console.assert(cameraRobot instanceof SpeedCamera); // pass2console.assert(cameraRobot instanceof Robot); // fail
The second approach works with respect to the instanceof
operation. Now SpeedCamera
is definitely an instance of Robot
. However, be aware that this approach will have some very nasty implications. Any changes on the SpeedCamera
prototype will also propagate to the Robot
prototype since they are essentially the same object. It's possible then to override a method on the super class directly from the subclass, which will certainly introduce some bugs further down the prototype chain.
The third approach is what we actually want. Now, the instanceof
operator will work correctly, and also report that our cameraRobot
is indeed a subclass of Robot
. So now when we call any of the methods on our subclass, the JavaScript runtime will search for the method and will travel along the prototype chain until it either encounters the method or it reaches the default top-level Object
, at which point it will return undefined
.
For example, if we were to invoke the following method: cameraRobot.captureImage()
the runtime will first check this.captureImage()
where the this
keyword will refer to the SpeedCamera
instance. Since the method will not be found, the runtime will proceed to look on the prototype, checking the Robot
instance referenced by SpeedCamera.prototype
. The method won't be found there, however, it will be found on Robot.prototype
which is the next step up.
Lastly, there is still one more thing we have to address. By setting the SpeedCamera
prototype directly, we are essentially blowing away the reference to the original constructor for instantiations. This can be observed with the following assertion:
1console.assert(cameraRobot.constructor === SpeedCamera); // fail
The implications of this is that any consumers of our SpeedCamera
class might expect the above to pass. It could also be useful to determine which function created our instance. So let's go ahead and fix it with the following:
1Object.defineProperty(SpeedCamera.prototype, "constructor", {2 enumerable: false,3 value: SpeedCamera,4 writable: true,5});
Essentially, what we've done here is defined a property on SpeedCamera.prototype
, named constructor
that is not enumerable. So it will not show up in for ( let prop in SpeedCamera ) {}
loops. Its value will be the value of the subclass itself, and it will also be writable. Now the following should all pass:
1console.assert(cameraRobot.constructor === SpeedCamera); // pass2console.assert(cameraRobot instanceof SpeedCamera); // pass3console.assert(cameraRobot instanceof Robot); // pass
There's still one more thing to be aware of. The instanceof
operator is slightly misleading. In JavaScript, this operator will simply check if the object prototype on the right side is found in the prototype chain of the object on the left side. That's it. That's all it does. The operator does not check instances but rather prototypes. JavaScript is a dynamic language, and we've directly manipulated the prototype of the objects above, so be careful when depending on instanceof
operations. Double check that the prototypes being compared are exactly what you would expect them to be within the given context.
ES6 classes simplify a lot of the functionality from above, take a look at the following:
1class Robot {2 constructor() {}34 turnOn() {}56 turnOff() {}78 isPoweredOn() {}910 captureImage() {}11}1213class SpeedCamera extends Robot {14 constructor() {15 super(); // Don't forget the super call!16 }1718 calculateObjectSpeed(x, y, z) {}19}
Notice that our previous assertions should still pass. All of the work of updating the SpeedCamera
prototype, and setting the constructor, is masked by the syntactical sugar that ES6 classes provide 🎉. Classes simplify a lot of things, but keep in mind that underneath it all, we still have the prototype chain.
There's one last thing that we should talk about, that we haven't really talked about yet. Consider the code blocks below, is there a difference between the two objects?
1// first block2function SpeedCamera() {}34SpeedCamera.prototype.calculateObjectSpeed = function (x, y, z) {};56// second block7function SpeedCamera() {8 this.calculateObjectSpeed = function (x, y, z) {};9}
The answer is yes. In the first code block, the calculateObjectSpeed
method is defined on the prototype of SpeedCamera
. This means that all instances of SpeedCamera
will share this one single method. The second code block is slightly different. Each instantiation of SpeedCamera
will get it's own personal calculateObjectSpeed
method, and therefore this method will not be shared across instances. Keep that in mind when implementing inheritance, as the second code block will naturally have a higher memory overhead.