dg.

Understanding JavaScript inheritance

6 minutes / 1131 words

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.

Inheritance with prototypes

Let's start off with a generic Robot object with the following methods:

1function Robot() {}
2
3Robot.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() {}
2
3SpeedCamera.prototype.calculateObjectSpeed = function (x, y, z) {};
4
5const 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};
8
9/** Option 2: Set the two prototypes equal to each other */
10SpeedCamera.prototype = Robot.prototype;
11
12/** 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); // pass
2console.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.

Rebuilding the chain

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); // pass
2console.assert(cameraRobot instanceof SpeedCamera); // pass
3console.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.

Inheritance with classes

ES6 classes simplify a lot of the functionality from above, take a look at the following:

1class Robot {
2 constructor() {}
3
4 turnOn() {}
5
6 turnOff() {}
7
8 isPoweredOn() {}
9
10 captureImage() {}
11}
12
13class SpeedCamera extends Robot {
14 constructor() {
15 super(); // Don't forget the super call!
16 }
17
18 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.

Reusing methods

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 block
2function SpeedCamera() {}
3
4SpeedCamera.prototype.calculateObjectSpeed = function (x, y, z) {};
5
6// second block
7function 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.


Have I made a mistake? Please consider submitting a pull request
Back to top