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"
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.
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..."