Extends and inheritance

Subclasses

A class can be of a more general level or of a more specific level. Take for instance a class "animal". Instances of this class may be "mammal", "reptile", "bird" etc. But they can also be viewed as subclasses of class "animal". The most specific level is an individual animal, like pet guinea pig Mummel, that may be an instance of class "rodent", class "mammal, class "animal", class "organism", depending on how finely graded and generic you want the classification to be.

Mummel may even be a direct instance of multiple classes, e.g., from both class "guinea pig" and class "pet" (that both may be subclasses of "animal")...

The extends keyword

In (class-based) OOP inheritance means that one class can "borrow" another class's properties and methods, while adding, overriding or enhancing certain parts with its own logic. The class becomes a subclass (child class) of its superclass (parent class).

We can use the extends keyword to extend a class like class Mammal extends Animal {}. Then derived class Mammal has access to superclass Animal's public properties and methods: mammals share, or "inherit", the general properties and functions that all animals have and can perform.

In the next example derived class Actor extends class Person:


class Person { 
  #name;
  
  constructor (name) { 
    this.#name = name;   
  }  
  
  get name() {
    return this.#name;
  }
}

class Actor extends Person { 
  #nAcademyAwards;
  
  constructor (name, nAcademyAwards) {
    super(name); // (will be explained later)
    this.#nAcademyAwards = nAcademyAwards;   
  }  
  
  get nAcademyAwards() {
    return this.#nAcademyAwards;
  }
}

const jackNicholson = new Actor("Jack Nicholson", 3);
 
console.log(jackNicholson.name) // logs: "Jack Nicholson"
console.log(jackNicholson.nAcademyAwards) // logs: 3

The prototype chain

In JavaScript classes are objects with prototype inheritance. To understand class inheritance it is important to understand how a superclass, subclass and instances inherit via the prototype chain. An example:


class Person {}
class Actor extends Person {}

console.log(Object.getPrototypeOf(Actor) === Person); // logs: true
console.log(Object.getPrototypeOf(Person) === Function.prototype); // logs: true
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); // logs: true

// Prototype chain of Actor:
// Actor --> Person --> Function.prototype --> Object.prototype --> null

const morganFreeman = new Actor();

console.log(Object.getPrototypeOf(morganFreeman) === Actor.prototype); // logs: true
console.log(Object.getPrototypeOf(Actor.prototype) === Person.prototype); // log: true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // log: true

// Prototype chain of morganFreeman:
// morganFreeman --> Actor.prototype --> Person.prototype --> Object.prototype --> null

console.log(Object.getPrototypeOf(Actor) === Person.prototype); // logs: false

As we can see in the above example, SuperClass.prototype becomes the prototype of SubClass.prototype. This is typical for extended classes. A class extension is not a construction of a subclass (otherwise the last statement in the above example would have been true). Extending constructors cannot be achieved with constructor functions, unless we do something ridiculous like:


function Superclass() {};
function Subclass() {};

Superclass.prototype.superMethod = function () {
  return "Hello world!";
};

Object.setPrototypeOf(Subclass, Superclass);
Object.setPrototypeOf(Subclass.prototype, Superclass.prototype);

const instance = new Subclass();

console.log(instance.superMethod()); // logs: "Hello world!"

BTW. This only works with properties set on Superclass.prototype. A this.property in Superclass will not be set on an instance of Subclass.

Back to instances constructed with an extended class. We can change the prototype chain of such instance, and in that way, change the inherited methods of an instance:


class Person {
  baseClassName = "Person";
  baseClassGreeting() { return "Hello, I'm a person" }
 }
class Actor extends Person {}

const morganFreeman = new Actor();
console.log(morganFreeman.baseClassName); // logs: "Person"
console.log(morganFreeman.baseClassGreeting()); // logs: "Hello, I'm a person"

class American {
  baseClassName = "American";
  baseClassGreeting() { return "Hello, I'm an American" }
}

// Change the inheritance chain:
Object.setPrototypeOf(Actor.prototype, American.prototype);
// New prototype chain of morganFreeman:
// morganFreeman --> Actor.prototype --> American.prototype --> Object.prototype --> null

console.log(morganFreeman.baseClassName); // logs: "Person"
console.log(morganFreeman.baseClassGreeting()); // logs: "Hello, I'm an American"

We can see that fields (baseClassName in the example above) become own properties of instances, they are not affected by a change in the prototype chain. The instance methods in a class (baseClassGreeting() in the example above) become methods of the MyClass.prototype. Changing the prototype chain may change the inherited methods of the instance.

Public instance fields of both superclass and subclass become own properties of an instance. Public instance methods become methods in the instance's prototype chain: public instance methods of a superclass become methods on the Superclass.prototype, public instance methods of a subclass become methods on the Subclass.prototype.


class Person {
  baseClassName = "Person";
  baseClassGreeting() { return "Hello, I'm a person" }  
}
class Actor extends Person {
  subClassGreeting() { return "Hello, I'm an actor" } 
}

const bestActor = new Actor();

console.log(bestActor.baseClassName); // logs: "Person"
console.log(Object.hasOwn(bestActor, 'baseClassName')); // logs: true

console.log(bestActor.baseClassGreeting()); // logs: "Hello, I'm a person"
console.log(Object.hasOwn(bestActor, 'baseClassGreeting')); // logs: false
console.log(Object.hasOwn(Person.prototype, 'baseClassGreeting')); // logs: true

console.log(bestActor.subClassGreeting()); // logs: "Hello, I'm an actor"
console.log(Object.hasOwn(bestActor, 'subClassGreeting')); // logs: false
console.log(Object.hasOwn(Actor.prototype, 'subClassGreeting')); // logs: true

Static members (both fields and methods) are inherited through the actual supper/sub classes being each other's prototypes. Private members remain private to the superclass.


class MySuperClass {
  static superField = 10;
  static superMethod(){ return 20 };
}
class MyDerivedClass extends MySuperClass {}

console.log(MyDerivedClass.superField); // logs: 10
console.log(MyDerivedClass.superMethod()); // logs: 20

// Next code throws a SyntaxError:
class MySuperClass {
  #superField = 10;
}
class MyDerivedClass extends MySuperClass {
  derivedField = this.#superField; // SyntaxError: reference to undeclared private field or method #superField
}

BTW. Replacing this in the above example by super will also throw an error. See next section.

Is instance of?

The instanceof operator tests to see if an object is, directly or indirectly, an instance of a class or constructor function. The statement obj instanceof constructor tests if constructor.prototype appears anywhere in the prototype chain of obj. The return value is a boolean value.

The isPrototypeOf() method does something similar: it checks if an object exists in another object's prototype chain. Any instanceof statement can be rephrased into an equivalent isPrototypeOf() statement, and vice versa.


class Superclass {}
class Subclass extends Superclass {}
const obj = new Subclass();

console.log(obj instanceof Subclass); // logs: true
console.log(obj instanceof Superclass); // logs: true
console.log(obj instanceof Object); // logs: true

console.log(Subclass.prototype.isPrototypeOf(obj)); // logs: true
console.log(Superclass.prototype.isPrototypeOf(obj)); // logs: true
console.log(Object.prototype.isPrototypeOf(obj)); // logs: true

console.log(Subclass.prototype instanceof Superclass); // logs: true
console.log(Superclass.prototype.isPrototypeOf(Subclass.prototype)); // logs: true

console.log(Subclass instanceof Superclass); // logs: false
console.log(Superclass.isPrototypeOf(Subclass)); // logs: true

Some things worth noticing:


const literalString = "This is a literal string";
const stringObject = new String("String created with constructor");

console.log(literalString instanceof String); // logs: false
console.log(stringObject instanceof String); // logs: true

console.log(String.prototype.isPrototypeOf(literalString)); // logs: false
console.log(String.prototype.isPrototypeOf(stringObject)); // logs: true

// ****

class MyClass {
  someField = "foo"
  someMethod() { return 2 }
}
const myObj = { __proto__: MyClass.prototype };

/*
The prototype of myObj is myClass.prototype, but
myObj was not constructed by MyClass, so fields
did not become properties of myObj. However,
methods in MyClass DO become methods of MyClass.prototype:
*/
console.log(myObj instanceof MyClass); // logs: true
console.log(myObj.someField); // logs: undefined
console.log(myObj.someMethod()); // logs: 2

The super keyword

Invoking a superclass's constructor

A constructor of a derived class (a class using keyword extends) must call the superclass constructor using super(), passing up any parameters that the superclass constructor is expecting. super() must be called from within the subclass constructor, as shown in the first example on this page. super() must be called before keyword this can be used.


class SuperClass {}

class DerivedClass extends SuperClass { 
  #prop; 
  constructor (x) {
    this.#prop = x; // ReferenceError: must call super constructor before using 'this' in derived class constructor
    super();
  } 
}

const myInstance = new DerivedClass();

A derived class does not necessarily need a constructor. It only needs one if the superclass constructor needs to be extended or altered.


class Shape {
  x;
  y;
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class Movable extends Shape {
  move(dx, dy) {
    this.x += dx;
    this.y += dy;
	return [this.x, this.y]
  }
}

const movableShape = new Movable(2,3);
console.log(movableShape.move(2,3)); // logs: [ 4, 6 ]

The super keyword can be used to invoke a superclass's constructor (a super(...args) expression in a class constructor, as shown above), but can also be used to access properties of an object's prototype (super.myProp and super[myExpr] expressions). This way we can access "super fields" and "super methods".

this vs. super

We can use the super keyword in expressions like super.myProp or super[myExpr]. This way we can point to fields and methods of a superclass from within a subclass field or method. But we can also achieve this with keyword this. What is the difference?

The value of this references a newly created instance, unless this is used in a static member. this.publicInstanceField accesses a (own) property of an instance, this.publicInstanceMethod accesses a method on an object in the prototype chain of an instance.


class MySuperClass {
  superField = 10;
  superMethod() { return 20; } 
}
class MyDerivedClass extends MySuperClass {
  derivedField = this.superField + 2; // this.superField accesses an own property of an instance
  derivedMethod() { return this.superMethod() + 2; } // this.superMethod() accesses a method on MySuperClass.prototype
}

const myInstance = new MyDerivedClass();

console.log(myInstance.derivedField); // logs: 12
console.log(myInstance.derivedMethod()); // logs: 22

The value of super references SuperClass.prototype, unless super is used in a static member. The reference of super in a static member is the superclass itself. This has a couple of consequences.

Since the reference of super used in instance fields is SuperClass.prototype, super cannot be used to access instance field of a superclass. Instance fields are set on the instance instead of on SuperClass.prototype. super can be used to access static fields (or static methods) of a superclass, but only from a static member.


class MySuperClass {
  static staticSuperField = 10;
  instanceSuperField = 20;
  superMethod() { return 30; } 
}
class MyDerivedClass extends MySuperClass {
  static staticDerivedField = super.staticSuperField;
  instanceDerivedField = super.instanceSuperField;
  derivedMethod() { return super.superMethod(); } 
}

const myInstance = new MyDerivedClass();

console.log(MyDerivedClass.staticDerivedField); // logs: 10
console.log(myInstance.instanceDerivedField); // logs: undefined
console.log(myInstance.derivedMethod()); // logs: 30

Replacing super by this in all three occurrences in the above example would have worked perfectly. The logs would have been 10, 20, 30. So why not always use this?

An example of when you might want to use super instead of this is when you do not want a method to behave differently when bound to another object. In the next example a method of a subclass is called on a different object. If super would have been replaced by this, the call() method would have changed the value of this. Now the call() method has no effect. Replace super by this in the example below, and the logs will be 1 and 2.


class Base {
  BaseMethod() {
    return 1;
  }
}
class Derived extends Base {
  getBaseMethod() {
    return super.BaseMethod();
  }
}

const myInstance = new Derived();

console.log(myInstance.getBaseMethod()); // logs: "1"
console.log(myInstance.getBaseMethod.call({
  BaseMethod() {
    return 2;
  }
})); // logs: "1"

BTW. This only holds for methods that get super.myProp, not for methods that set super.myProp. See "Different syntactic constructs" here below.

Shadowing and overriding properties

As covered before, properties with equal names on different objects in the prototype chain creates shadowed properties. An instance inherits shadowed properties if superclass and subclass both have different public instance methods but with the same name: The method on SuperClass.prototype gets shadowed by the method on SubClass.prototype. The superclass method is still in the prototype chain of instances of the subclass, but is not directly accessible through an subclass instance. However, the shadowed method is still directly accessible through instances of the superclass! This is called subtype polymorphism.

If superclass and subclass both have a different field with the same name, then the subclass field will override the superclass field. Properties will not be shadowed.


class MySuperClass {
  myField = 10;
  myMethod() { return 10; } 
}
class MyDerivedClass extends MySuperClass {
  myField = 20;
  myMethod() { return 20; } 
}

const myInstance = new MyDerivedClass();

console.log(myInstance.myField); // logs: 20
console.log(myInstance.myMethod()); // logs: 20
console.log(MySuperClass.prototype.myMethod()); // logs: 10

We can use this, for instance, to augment or enhance a superclass method. In the next example subclass method introduce() "overrides" the superclass method with the same name.


class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }

  introduce(phrase) {
    if(typeof phrase === "string") {
      return phrase;
	} else { 
      return `Hello, I am ${this.#name}.`;
	}
  }
}
class Polyglot extends Person { 
  #nLanguages; 
  constructor(name, n) {
    super(name);
    this.#nLanguages = n;
  }
  // @Override
  introduce(phrase) {
    let myPhrase = super.introduce(phrase);
    phrase = `${myPhrase} I speak ${this.#nLanguages} languages.`;
    return phrase;
  }
}

const sam = new Polyglot("Sam", 3);
console.log(sam.introduce()); // logs: "Hello, I am Sam. I speak 3 languages."

BTW. The non-standard // @Override comment in the example above indicates that the following method overrides the one in the superclass. See "Decorators" for more information.

Extending built-in classes

We can also extend build-in classes like Array and Map. Next example extends the built-in Date object.


class MyDate extends Date {
  getMyDateFormat() {
    const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    return `${this.getDate()}-${months[this.getMonth()]}-${this.getFullYear()}`;
  }
}

const now = new MyDate();
console.log(now.getMyDateFormat()); // logged: "22-Feb-2022"

Source: GitHub - GoogleChrome/samples/classes-es6/.

Also build-in classes have static properties and methods and instance properties and methods. Instance methods that return an object, such as Array.prototype.filter(), return an instance of the subclass instead of an instance of the build-in class.


class MyArray extends Array { myProperty = "Not an Array property" }
const arrayInstance = new MyArray(2, 4, 6, 8, 10);
const filteredArray = arrayInstance.filter(elm => elm >= 5);

console.log(filteredArray); // logs: [ 6, 8, 10 ]
console.log(filteredArray.myProperty); // logs: "Not an Array property"

console.log(arrayInstance.constructor === filteredArray.constructor); // logs: true
console.log(filteredArray.constructor === MyArray); // logs: true
console.log(Object.getPrototypeOf(arrayInstance) === MyArray.prototype); // logs: true
console.log(Object.getPrototypeOf(filteredArray) === MyArray.prototype); // logs: true

// Array.from() is a static Array method:
console.log(Array.from('foo')); // logs: [ "f", "o", "o" ]
console.log(MyArray.from('foo')); // logs: [ "f", "o", "o" ]

Multilevel and Multiple inheritance

With multiple inheritance a class can directly inherit from multiple independent parent classes. For example, you might want a Circle class both to be a subclass of a PlaneShapes class and a subclass of a ParametricEquations class.

JavaScript does not support multiple inheritance, it only supports single inheritance. After all, in JavaScript an object has only one prototype. However, by using composition (see next chapter) you can implement some of the functionality of true multiple inheritance and, in addition, avoid the increased complexity and ambiguity (the diamond problem) that multiple inheritance often cause.

Multilevel and Multiple inheritance diamond inheritance multilevel single inheritance
Diagram visualization of "diamond inheritance" (multiple inheritance) and multilevel single inheritance. The rectangles symbolize classes and the arrows symbolize inheritance.

In a multilevel inheritance hierarchy any superclass can inherit from another superclass of a more general level. JavaScript does support multilevel inheritance; JavaScript supports multilevel single inheritance. Note that a superclass can have multiple direct subclasses, but every subclass directly inherits from a single parent superclass.


class Supersuperclass { msg = "This comes from super, super deep..." }
class Superclass extends Supersuperclass {}
class Subclass extends Superclass {}
const instance = new Subclass();
console.log(instance.msg); // logs: "This comes from super, super deep..."