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:
- read the first example in this section on subclassing with extends
- the extends keyword
- Class inheritance which has some nice pictures
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.