Inheritance with extends
Inheritance lets one class build on another, reusing its data and behavior while adding or refining its own. In JavaScript you express this with the extends keyword, which creates a subclass (child) that derives from a superclass (parent). This is the cleanest way to model “is-a” relationships and avoid duplicating logic across related types.
Extending a class
A subclass declared with extends automatically inherits all the methods of its parent through the prototype chain. You only write the parts that differ.
class Animal {
constructor(name) {
this.name = name;
}
describe() {
return `${this.name} is an animal.`;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
fetch() {
return `${this.name} fetches the ball.`;
}
}
const rex = new Dog("Rex");
console.log(rex.describe());
console.log(rex.fetch());
Output:
Rex is an animal.
Rex fetches the ball.
Dog never defines describe() or a constructor, yet both work — they are looked up on Animal.prototype. Dog adds only the new fetch() method.
Overriding methods
A subclass can redefine an inherited method by declaring a method with the same name. The subclass version shadows the parent’s, so calls on an instance resolve to the most specific implementation.
class Cat extends Animal {
speak() {
return `${this.name} says meow.`;
}
}
const felix = new Cat("Felix");
console.log(felix.speak());
console.log(felix.describe());
Output:
Felix says meow.
Felix is an animal.
Cat.speak() overrides Animal.speak(), while describe() is still inherited unchanged. This is polymorphism: code that calls animal.speak() gets the right behavior for whatever subclass it actually holds.
Calling parent methods with super
Overriding often means extending the parent’s behavior rather than replacing it entirely. Inside a method, super.methodName() invokes the parent’s version, letting you wrap it.
class Bird extends Animal {
speak() {
return `${super.speak()} It chirps.`;
}
}
const robin = new Bird("Robin");
console.log(robin.speak());
Output:
Robin makes a sound. It chirps.
When a subclass defines its own constructor, it must call super(...) before touching this. super() runs the parent constructor so inherited fields are initialized first.
class Animal {
constructor(name) {
this.name = name;
}
describe() {
return `${this.name} is an animal.`;
}
}
class Puppy extends Animal {
constructor(name, age) {
super(name); // initialize the parent first
this.age = age; // then add subclass state
}
describe() {
return `${super.describe()} It is ${this.age} months old.`;
}
}
const buddy = new Puppy("Buddy", 4);
console.log(buddy.describe());
Output:
Buddy is an animal. It is 4 months old.
Referencing
thisbefore callingsuper()in a subclass constructor throws aReferenceError. Always callsuper()first.
A small hierarchy
Hierarchies can be more than two levels deep. Each link in the chain extends the one above it, and method lookups walk up the chain until a match is found.
class Shape {
area() {
return 0;
}
toString() {
return `${this.constructor.name} with area ${this.area()}`;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(side) {
super(side, side);
}
}
const shapes = [new Rectangle(3, 4), new Square(5)];
for (const shape of shapes) {
console.log(shape.toString());
}
Output:
Rectangle with area 12
Square with area 25
Square extends Rectangle, which extends Shape. The inherited toString() calls area(), which dynamically resolves to each subclass’s implementation.
Inheritance terms at a glance
| Term | Meaning |
|---|---|
extends | Declares that a class inherits from another |
| Superclass / parent | The class being extended |
| Subclass / child | The class doing the extending |
| Override | Redefining an inherited method in a subclass |
super(...) | Calls the parent constructor (in a subclass constructor) |
super.method() | Calls the parent’s version of a method |
Best practices
- Use inheritance only for true “is-a” relationships; prefer composition for “has-a” or behavior sharing across unrelated types.
- Keep hierarchies shallow — deep chains are hard to reason about and refactor.
- Always call
super()first in a subclass constructor before referencingthis. - Override to extend with
super.method()when you want parent behavior plus your own. - Design parent methods so subclasses can safely override them (small, focused, well-named).
- Reach for mixins when you need to share behavior across classes that don’t fit a single hierarchy.