Inheritance in JavaScript

Object-oriented programming (OOP) has many advantages:

  • data abstraction: objects hide implementation and provide behavior
  • polymorphism: objects can provide similar behavior for different types

In addition, OOP often provides for inheritance. That is, the behavior of a class can be extended or modified in subclasses.

We'll see how all these play out in JavaScript.

A good companion reading to this is the MDN article on Inheritance in JavaScript. The article has some good but different examples. You can consider that reading to be optional.

Note: in this reading, I'll link to short extra pages to show/demonstrate different examples. Please click through and read those.

Data Abstraction

Suppose we are using JavaScript for a 2D computer graphics system. In our system, we're going to have rectangles.

How shall we represent them? We could do any of the following:

  • upper left and lower right corners
  • upper left corner, width and height
  • any two corners
  • center coordinates and width and height
  • ...

What if we decide on a representation and change our minds later? Will that affect the users of our system? Will it affect the code for other coders, who import our software?

What kind of behavior (methods) do we want to support? We might list quite a lot of methods, including drawing it on the screen (which we will not discuss unless you twist my arm). For now, we're only going to provide the area method.

Regardless of how we represent the rectangle internally, how shall we allow the user to construct a rectangle?

For the purposes of this example, suppose we allow the user to give any two corners in the constructor.

Furthermore (switching to the other side of the abstraction barrier), let's suppose that we decide to implement the representation as the upper left and lower right corners.

Here's a JavaScript implementation of rectangles using the new class syntax. Please read the code; it's only about 20 lines.

Abstraction Barrier

The client of these rectangle objects only knows that they can report their area. They don't know or care what the internal representation is. This is called an abstraction barrier.

The implementor of these rectangle objects provides a constructor and that behavior (method).

Note that, the implementor might decide that, if clients ask for the area a lot, maybe it would make sense to pre-compute it, or switch to a representation that doesn't require so much computation. The behavior remains the same, but it's now faster and more efficient. We have the freedom to change our representation.

Polymorphism

Suppose that we also want to have circles. They should also support an area method. What will the internal representation be?

  • Center and radius?
  • Top and bottom points?
  • Left and right points?

Here's an implementation of rectangles and circles. Please read the code. The new circle code is only two dozen lines.

Note at the end of that code that we have a list of objects, some of the rectangles and some of the circles. We can compute the area of all of them by using the area method that they both support.

This is called polymorphism: a single interface to entities of different types.

One thing that's great about polymorphism is that we don't have to have a "master" function that knows how to compute the area of all kinds of shapes (and which then needs to be updated if we add a new kind of shape). Instead, the knowledge of how to compute the area of a shape is distributed among the shapes. Each knows how to compute its own area.

Inheritance

What if we have some behavior that is common to both rectangles and circles? Since we are supposing that this a 2D graphics system, maybe each has to keep track of its color (fill mode, stroke width and many other such properties).

We will define a new class called Shape. It will take a color as its argument. We'll also define methods to get and set the color and one to help print a shape, overriding the toString() method that every object inherits from Object, the ancestor of all JavaScript objects.

Please read this linked file on shapes and look in the JS console.

The constructor property

As you surely noticed, an object can know what function constructed it, using this.constructor. For s1 it's the Shape function. Functions have properties like name, which we used here in our toString() method.

Using the constructor name is not common, but it clarifies some of the issues we will discuss next.

Defining Subclasses

We'll redefine our classes for rectangles and circles to use inheritance. In honor of this change, we'll call them Rectangle and Circle instead of Rect and Circ.

Both of our Rectangle and Circle classes will inherit from Shape. (Shape will be described as the parent class or sometimes the superclass. Rectangle and Circle will be described as the child class or the subclass.)

In the old JavaScript OOP syntax, setting up inheritance requires several complicated steps, which I'll cover in an optional appendix. So, I'm going to skip the old syntax and leave it to an appendix, below.

For your work in this course, you should always use the new syntax.

New Syntax

In JavaScript/ECMAScript 2015, a new syntax was introduced for defining classes. At the same time, they introduced a new syntax for inheritance, namely the extends keyword to the new class syntax. For a quick introduction, see the following:

One important thing to note is that you must invoke the constructor for the parent class using super(); that invocation is not automatic. Typically, you would do this right away in the constructor for the subclass (the child class), as we will in the Shapes example below, but you can wait to initialize some other things first if you need to. You can also use super in method definitions, which is very cool, which invokes the inherited but overridden method.

Shapes Example

The following file re-implements the shapes from above, using the new syntax and also the rectangles, circles and triangles.

Here's the file: shapes-new.html

Here's the JS code from that file:

class Shape {
    constructor(color) {
        this.color = color;
    }
    getColor() {
        return this.color;
    }
    setColor(color) {
        this.color = color;
    }
    toString() {
        return "[A "+this.color+" "+this.constructor.name+"]";
    }
}

var s1 = new Shape("red");
console.log("s1 is "+s1.toString());     // Look in the JS console

// ================================================================

class Rectangle extends Shape {
    constructor(color,corner1,corner2) {
        super(color);
        let c1 = corner1;
        let c2 = corner2;
        this.ulx = Math.min(c1.x, c2.x);
        this.uly = Math.max(c1.y, c2.y);
        this.lrx = Math.max(c1.x, c2.x);
        this.lry = Math.min(c1.y, c2.y);
    }
    
    area() {
        let width = this.lrx - this.ulx;
        let height = this.uly - this.lry;
        return width*height;
    }
}

var origin = {x: 0, y: 0};
var p1 = {x: 10, y: 20};

var r1 = new Rectangle( "blue", origin, p1);
console.log("r1 is "+r1.toString());
console.log("area is "+r1.area());

// ================================================================

class Circle extends Shape {
    constructor (color,c1,radius) {
        super(color);
        this.center = c1;
        this.radius = radius;
    }
    area() {
        let radius = this.radius;
        return Math.PI*radius*radius;
    }
}
        
var c1 = new Circle( "green", origin, 10);
console.log("c1 is "+c1.toString());
console.log("area of c1 is "+c1.area());

var objs = [ r1, c1, 
             new Rectangle("teal", p1, {x: 5, y: 15}), 
             new Circle( "yellow", p1, 100) ];

objs.forEach( function (obj, i) {
    console.log(i+" area of "+obj+" is "+obj.area());
});

// ================================================================

class Triangle extends Shape {
    constructor (color,v1,v2,v3) {
        super(color);
        this.v1 = v1;    
        this.v2 = v2;    
        this.v3 = v3;
    }

    // area formula from https://en.wikipedia.org/wiki/Triangle
    // and https://sciencing.com/area-triangle-its-vertices-8489292.html
    area() {
        let a = this.v1;
        let b = this.v2;
        let c = this.v3;
        return 0.5*Math.abs(a.x * (b.y-c.y) +
                            b.x * (c.y-a.y) +
                            c.x * (a.y-b.y));
    }
}

var p2 = {x: 7, y: 11};
var p3 = {x: 9, y: 3};

var t1 = new Triangle("mauve",p1, p2, p3);
console.log("t1 is "+t1.toString());

// ================================================================
// Checking the inheritance chain for r1

// All true
console.log("r1 instanceof Rectangle: "+(r1 instanceof Rectangle));
console.log("r1 instanceof Shape: "+(r1 instanceof Shape));
console.log("r1 instanceof Object: "+(r1 instanceof Object));

// False
console.log("r1 instanceof Circle: "+(r1 instanceof Circle));

The instanceof Operator

There is a special JavaScript operator that will go through the prototype chain for an object to determine whether it inherits from a particular class.

// All true
console.log("r1 instanceof Rectangle: "+(r1 instanceof Rectangle));
console.log("r1 instanceof Shape: "+(r1 instanceof Shape));
console.log("r1 instanceof Object: "+(r1 instanceof Object));

// False
console.log("r1 instanceof Circle: "+(r1 instanceof Circle));

Summary

Many object-oriented languages, such as Java and Python, have classes and objects, with classes being the templates and factories for objects. JavaScript doesn't have this "classical" (as Douglas Crockford, author of JavaScript: The Good Parts, describes it) inheritance system, but instead uses prototype chains. Nevertheless, we can implement this classical OOP inheritance in JavaScript, and many JavaScript libraries do so. For example, Threejs, a library for doing 3D computer graphics in JavaScript, has OOP inheritance like this.

You can stop here, but if you're curious about the old syntax, read on.

Appendix on Inheritance in the Old Syntax

Here's a high-level view of creating a class and a subclass:

// constructor for superclass
function Shape(color) {
   this.color = color;
}

// constructor for subclass
function Rectangle(color,c1,c2) {
    // initialize using superclass, passing the instance and necessary arg
    Shape.call(this,color);
    ...
}

Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle; 

Setting up inheritance requires:

  • To properly initialize a new object, we must invoke the parent's constructor from the child's constructor. True, we could assign the color instance variable ourselves, but what if the parent changes the initialization code, say to check the color? Then we'd have to copy those changes to every child: very bad.
  • We have to specify that the child's prototype is an instance of Shape. Since each object inherits instance variables and methods from its prototype chain, we have to make sure that chain is correct.
  • Setting the child's prototype has the unwanted side-effect of changing the constructor property, which we use in the name, so we set it back to the correct value.

All this trouble about the prototype chain makes the old OOP syntax very difficult and is a major advantage of the new syntax.

Here we see a good use of the call method that functions have. We invoke the Shape function but we supply a value to use for this (it happens to be the same as the current this) and any additional arguments it needs.

The following file has a complete implementation of these ideas, with rectangles and circles. It also adds Triangles, but it "forgets" to set the prototype and prototype constructor. The result is that triangles don't inherit from Shape, which you can see in the console.log output.

Here's the file: shapes.html

Please skim the code; it's not terribly long, and much of it is familiar to you from the earlier examples, so you can focus on the differences.

Notice that one of the cool things about inheritance is code reuse: all the children (grandchildren and further descendants) of Shape get its methods.