The extends keyword
extends creates a child class and links it to the parent class through prototypes: instances of the child receive the parent methods, and the child class itself receives the parent static members. Together with super() this is the complete inheritance mechanism of ES6 classes.
Theory
TL;DR
class B extends A {}performs two links:B.prototype.__proto__ = A.prototypeandObject.setPrototypeOf(B, A).- The first link gives inheritance of instance methods, the second gives inheritance of static methods and fields.
- If the child declares its own
constructor, it must callsuper()before the first access tothis. - An overridden method can call the parent implementation through
super.methodName(). - To the right of
extendsyou may put any expression that returns a constructor: a class, a constructor function, or the result of a factory call.
Quick example
class Animal {
speak() {
console.log("The animal makes a sound");
}
}
class Dog extends Animal {
bark() {
console.log("Woof-woof!");
}
}
const dog = new Dog();
dog.speak(); // "The animal makes a sound", the method is inherited
dog.bark(); // "Woof-woof!"Dog inherits every method of Animal, because lookup walks the chain Dog.prototype, then Animal.prototype, then Object.prototype.
What extends does under the hood
When you write:
class B extends A {}the engine performs the equivalent of two operations:
B.prototype.__proto__ = A.prototype; // instance methods
Object.setPrototypeOf(B, A); // static methods and fieldsTwo consequences follow:
- instance methods (
B.prototype.method) are looked up inA.prototypewhen they are missing fromB.prototype; - static members (
A.staticMethod) are reachable throughBas well.
So extends builds two different chains at once. For an instance:
// instance of B -> B.prototype -> A.prototype -> Object.prototype -> nullAnd a separate chain for the classes themselves, which are function objects:
// B -> A -> Function.prototype -> Object.prototype -> nullThe child constructor and super()
A child class can call the parent constructor through super().
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // calls the Animal constructor
this.breed = breed;
}
}
const dog = new Dog("Rex", "shepherd");
console.log(dog.name); // "Rex"
console.log(dog.breed); // "shepherd"Important:
- if you declare your own
constructorin the child, callingsuper()is mandatory before any use ofthis; - without it you get
ReferenceError: Must call super constructor before accessing 'this'; - if there is no explicit constructor, the engine supplies
constructor(...args) { super(...args); }itself.
Overriding methods and super.methodName()
A parent method can be overridden while still calling the old implementation through super.methodName().
class Animal {
speak() {
console.log("The animal speaks");
}
}
class Dog extends Animal {
speak() {
super.speak(); // calls the parent implementation
console.log("The dog barks");
}
}
new Dog().speak();
// The animal speaks
// The dog barksInheriting static methods and fields
If the parent has static members, they are inherited too:
class Animal {
static category = "Animals";
static info() {
console.log("This is the base class for animals");
}
}
class Dog extends Animal {}
console.log(Dog.category); // "Animals"
Dog.info(); // "This is the base class for animals"This works precisely because of the second link: the lookup for Dog.category does not find it on Dog itself and climbs up to Animal.
Inheriting from functions and dynamic expressions
You can inherit not only from classes but also from plain constructor functions:
function Vehicle(name) {
this.name = name;
}
Vehicle.prototype.drive = function () {
console.log(`${this.name} is driving`);
};
class Car extends Vehicle {}
const car = new Car("BMW");
car.drive(); // "BMW is driving"
extendsworks with any value that has aprototype, not necessarily with aclass.
The expression on the right is evaluated when the class is declared, so the base class can be chosen dynamically:
function getBaseClass(type) {
return type === "cat"
? class Animal {
speak() { console.log("Meow"); }
}
: class Animal {
speak() { console.log("Woof"); }
};
}
class Pet extends getBaseClass("cat") {}
new Pet().speak(); // "Meow"This is a powerful technique: mixins, adapters and model factories all rest on it.
A summary of what extends gives you:
What extends does | Description |
|---|---|
| Builds the prototype chain | Child.prototype inherits from Parent.prototype |
| Lets you call the parent constructor | through super() |
| Lets you call parent methods | through super.methodName() |
| Inherits static methods and fields | Child.staticMethod() |
| Works not only with classes | but with constructor functions too |
| Accepts dynamic expressions | class X extends getBaseClass() |
Common mistakes
- Touching
thisin the child constructor before callingsuper(). That is always aReferenceError, notundefined. - Believing that statics are not inherited. In fact
Dog.info()works with no extra code. - Trying to inherit from an arrow function or from a plain object: they have no
prototype, so you get aTypeError. - Calling the parent method as
Animal.prototype.speak.call(this)instead ofsuper.speak(). It works, but it breaks as soon as the hierarchy changes. - Confusing the two chains:
Dog.__proto__isAnimal, whileDog.prototype.__proto__isAnimal.prototype.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.