Inheritance
Learn how prototypal and class-based inheritance work in JavaScript and how to build object hierarchies.
Inheritance
In the previous topic we learned how the prototype chain works — how JavaScript looks up properties by walking up a chain of prototypes. Inheritance is how you deliberately set up that chain so one object or class gets the behavior of another.
JavaScript supports inheritance in two ways:
- Prototypal inheritance — the old way, using constructor functions and
Object.create() - Class-based inheritance — the modern way, using
classandextends
Both work through the same prototype chain under the hood. Classes are just cleaner syntax.
Why Inheritance
Without inheritance, shared behavior means copying code everywhere.
const ali = {
name: "Ali",
greet() { return `Hello, I am ${this.name}.`; },
eat(food) { return `${this.name} eats ${food}.`; }
};
const sara = {
name: "Sara",
greet() { return `Hello, I am ${this.name}.`; }, // exact copy
eat(food) { return `${this.name} eats ${food}.`; } // exact copy
};Same methods duplicated everywhere. Inheritance solves this — define shared behavior once, let many objects use it.
Class-Based Inheritance — extends
The modern approach. A child class extends a parent class and inherits all its properties and methods.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I am ${this.name}.`;
}
describe() {
return `${this.name} is ${this.age} years old.`;
}
toString() {
return `Person(${this.name}, ${this.age})`;
}
}
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // call parent constructor first
this.grade = grade;
}
study(subject) {
return `${this.name} is studying ${subject}.`;
}
describe() {
// Override parent method — add extra info
return `${super.describe()} They are in grade ${this.grade}.`;
}
}
class Employee extends Person {
constructor(name, age, department, salary) {
super(name, age);
this.department = department;
this.salary = salary;
}
work() {
return `${this.name} is working in ${this.department}.`;
}
describe() {
return `${super.describe()} They work in ${this.department}.`;
}
}
const student = new Student("Ali", 20, "A");
const employee = new Employee("Sara", 28, "Engineering", 150000);
console.log(student.greet()); // Hello, I am Ali. — from Person
console.log(student.study("Math")); // Ali is studying Math. — from Student
console.log(student.describe()); // Ali is 20 years old. They are in grade A.
console.log(employee.greet()); // Hello, I am Sara.
console.log(employee.work()); // Sara is working in Engineering.
console.log(employee.describe()); // Sara is 28 years old. They work in Engineering.
console.log(student instanceof Student); // true
console.log(student instanceof Person); // true
console.log(student instanceof Employee); // falsesuper in Detail
super has two uses — calling the parent constructor and calling parent methods.
In the constructor
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
this.alive = true;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name, "woof"); // pass name to Animal, hardcode sound
this.breed = breed; // ✅ can use this after super()
}
}
const rex = new Dog("Rex", "Labrador");
console.log(rex.name); // Rex
console.log(rex.sound); // woof
console.log(rex.breed); // LabradorIn a child class constructor, super() must be called before any use of this. JavaScript throws a ReferenceError if you access this before calling super().
In methods
class Shape {
constructor(color) {
this.color = color;
}
describe() {
return `A ${this.color} shape`;
}
area() {
return 0;
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
describe() {
// Extend parent description — don't repeat it
return `${super.describe()} — circle with radius ${this.radius}`;
}
}
class Rectangle extends Shape {
constructor(color, width, height) {
super(color);
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
describe() {
return `${super.describe()} — rectangle ${this.width}x${this.height}`;
}
}
const circle = new Circle("red", 5);
const rect = new Rectangle("blue", 4, 6);
console.log(circle.describe()); // A red shape — circle with radius 5
console.log(circle.area().toFixed(2)); // 78.54
console.log(rect.describe()); // A blue shape — rectangle 4x6
console.log(rect.area()); // 24Multi-Level Inheritance
Inheritance chains can go multiple levels deep.
class Vehicle {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.speed = 0;
}
accelerate(amount) {
this.speed += amount;
return this;
}
brake(amount) {
this.speed = Math.max(0, this.speed - amount);
return this;
}
describe() {
return `${this.year} ${this.make} ${this.model}`;
}
}
class Car extends Vehicle {
constructor(make, model, year, doors) {
super(make, model, year);
this.doors = doors;
this.type = "car";
}
describe() {
return `${super.describe()} (${this.doors}-door car)`;
}
}
class ElectricCar extends Car {
constructor(make, model, year, doors, batteryRange) {
super(make, model, year, doors);
this.batteryRange = batteryRange;
this.type = "electric car";
}
charge() {
console.log(`Charging ${this.make} ${this.model}...`);
return this;
}
describe() {
return `${super.describe()} — electric, ${this.batteryRange}km range`;
}
}
const tesla = new ElectricCar("Tesla", "Model 3", 2024, 4, 570);
tesla.accelerate(60).accelerate(40);
console.log(tesla.speed); // 100
console.log(tesla.describe()); // 2024 Tesla Model 3 (4-door car) — electric, 570km range
tesla.charge(); // Charging Tesla Model 3...
console.log(tesla instanceof ElectricCar); // true
console.log(tesla instanceof Car); // true
console.log(tesla instanceof Vehicle); // truePrototypal Inheritance — The Old Way
Before classes, inheritance was done with constructor functions and manual prototype setup. You will see this in older codebases so it is worth understanding.
// Parent constructor
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound.`;
};
// Child constructor
function Dog(name, breed) {
Animal.call(this, name); // call parent constructor
this.breed = breed;
}
// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // fix constructor reference
// Add child methods
Dog.prototype.bark = function() {
return `${this.name} barks!`;
};
const rex = new Dog("Rex", "Labrador");
console.log(rex.speak()); // Rex makes a sound. — from Animal.prototype
console.log(rex.bark()); // Rex barks! — from Dog.prototype
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // trueThree manual steps:
Animal.call(this, name)— run the parent constructor on the childObject.create(Animal.prototype)— set up the prototype chain- Fix the
constructorproperty —Object.createbreaks it
ES6 classes do all three automatically. This is why classes are preferred.
Mixins — Multiple Behaviors
JavaScript does not support multiple inheritance — a class can only extend one parent. Mixins are a pattern to compose behavior from multiple sources.
// Behavior mixins — plain objects with methods
const Serializable = {
serialize() {
return JSON.stringify(this);
},
toJSON() {
return { ...this };
}
};
const Validatable = {
validate() {
return Object.keys(this).every(key => this[key] !== null && this[key] !== undefined);
},
getErrors() {
return Object.keys(this)
.filter(key => this[key] === null || this[key] === undefined)
.map(key => `${key} is required`);
}
};
const Loggable = {
log() {
console.log(`[${new Date().toISOString()}]`, this.constructor.name, JSON.stringify(this));
}
};
// Mixin function — copies methods onto a class prototype
function mixin(target, ...sources) {
Object.assign(target.prototype, ...sources);
return target;
}
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, I am ${this.name}.`;
}
}
// Apply multiple mixins
mixin(User, Serializable, Validatable, Loggable);
const user = new User("Ali", "ali@example.com");
console.log(user.greet()); // Hello, I am Ali.
console.log(user.serialize()); // {"name":"Ali","email":"ali@example.com"}
console.log(user.validate()); // true
user.log(); // [2024-03-15T...] User {"name":"Ali",...}
const incomplete = new User(null, "ali@example.com");
console.log(incomplete.validate()); // false
console.log(incomplete.getErrors()); // ["name is required"]Composition Over Inheritance
Inheritance is powerful but can be overused. When class hierarchies get too deep, they become rigid and hard to change.
Composition — building objects from small, focused pieces of behavior — is often a better approach.
// Instead of deep inheritance chains — compose behavior
function createAnimal(name) {
return { name };
}
function withEating(animal) {
return {
...animal,
eat(food) {
return `${animal.name} eats ${food}.`;
}
};
}
function withSwimming(animal) {
return {
...animal,
swim() {
return `${animal.name} is swimming.`;
}
};
}
function withFlying(animal) {
return {
...animal,
fly() {
return `${animal.name} is flying.`;
}
};
}
// Compose freely — mix any behaviors
const duck = withFlying(withSwimming(withEating(createAnimal("Donald"))));
const fish = withSwimming(withEating(createAnimal("Nemo")));
const eagle = withFlying(withEating(createAnimal("Sam")));
console.log(duck.eat("bread")); // Donald eats bread.
console.log(duck.swim()); // Donald is swimming.
console.log(duck.fly()); // Donald is flying.
console.log(fish.swim()); // Nemo is swimming.
console.log(eagle.fly()); // Sam is flying.No inheritance hierarchy needed. Any combination of behaviors is possible just by composing functions.
The rule of thumb — use inheritance when there is a genuine "is-a" relationship (a Dog IS an Animal). Use composition when you just want to share behavior (a Duck CAN swim AND fly). Prefer composition when in doubt — it is more flexible.
A Real Example — UI Component System
class Component {
constructor(id) {
this.id = id;
this.element = null;
this.isVisible = true;
}
render() {
throw new Error(`${this.constructor.name} must implement render()`);
}
mount(container) {
this.element = document.createElement("div");
this.element.id = this.id;
this.element.innerHTML = this.render();
container.appendChild(this.element);
return this;
}
show() {
if (this.element) this.element.style.display = "";
this.isVisible = true;
return this;
}
hide() {
if (this.element) this.element.style.display = "none";
this.isVisible = false;
return this;
}
update() {
if (this.element) {
this.element.innerHTML = this.render();
}
return this;
}
}
class Button extends Component {
constructor(id, label, onClick) {
super(id);
this.label = label;
this.onClick = onClick;
this.clickCount = 0;
}
render() {
return `<button class="btn">${this.label} (${this.clickCount})</button>`;
}
mount(container) {
super.mount(container);
this.element.querySelector("button").addEventListener("click", () => {
this.clickCount++;
this.onClick(this.clickCount);
this.update();
});
return this;
}
}
class Card extends Component {
constructor(id, title, content) {
super(id);
this.title = title;
this.content = content;
}
render() {
return `
<div class="card">
<h2>${this.title}</h2>
<p>${this.content}</p>
</div>
`;
}
updateContent(newContent) {
this.content = newContent;
this.update();
return this;
}
}
class Modal extends Card {
constructor(id, title, content) {
super(id, title, content);
this.isOpen = false;
}
render() {
return `
<div class="modal ${this.isOpen ? "open" : ""}">
<div class="modal-content">
<button class="close-btn">×</button>
${super.render()}
</div>
</div>
`;
}
open() {
this.isOpen = true;
this.update();
return this;
}
close() {
this.isOpen = false;
this.update();
return this;
}
}
// Usage
const container = document.getElementById("app");
const btn = new Button("like-btn", "Like", (count) => {
console.log(`Liked ${count} times!`);
});
const card = new Card("info-card", "JavaScript", "The language of the web.");
const modal = new Modal("info-modal", "About", "This is a modal dialog.");
btn.mount(container);
card.mount(container);
modal.mount(container);
card.updateContent("Now updated content.");
modal.open();Component is the base with shared behavior — mounting, showing, hiding, updating. Button, Card, and Modal extend it with specific rendering and behavior. Modal extends Card — using super.render() to build on it.
Summary
- Inheritance lets one class or object get the behavior of another through the prototype chain
- Use
extendsto create a child class —super()to call the parent constructor super.method()calls the parent version of an overridden method- Always call
super()before usingthisin a child constructor - Prototypal inheritance — old way with
Object.create()and constructor functions — classes are cleaner syntax for the same thing instanceofwalks the full prototype chain — a Dog instance is also an Animal instance- Mixins — copy methods from multiple sources onto a class prototype — work around single inheritance
- Composition — build objects from small pieces of behavior — more flexible than deep inheritance
- Use inheritance for genuine "is-a" relationships — use composition for "can-do" behaviors