Inheritance is one of the four pillars of OOP, allowing one class or object to acquire the properties and methods of another. JavaScript supports inheritance through both its prototypal inheritance model and the more familiar class-based syntax (ES6+). However, inheritance is just one tool for code reuse. Modern JavaScript favors composition over inheritance — building complex behavior by combining simpler objects rather than creating deep class hierarchies.
Key Insight: Inheritance is about "is-a" relationships (a Dog is an Animal). Composition is about "has-a" relationships (a Car has an Engine). Favor composition when relationships are complex or multiple.
JavaScript's core inheritance mechanism is prototype-based. Objects inherit directly from other objects.
// Parent object
const animalPrototype = {
eat() {
console.log(`${this.name} is eating`);
},
sleep() {
console.log(`${this.name} is sleeping`);
}
};
// Child object inheriting from parent
const dog = Object.create(animalPrototype);
dog.name = "Rex";
dog.bark = function() {
console.log(`${this.name} says: Woof!`);
};
dog.eat(); // Inherited: "Rex is eating"
dog.bark(); // Own: "Rex says: Woof!"dog
├─ name: "Rex"
├─ bark: function()
└─ [[Prototype]] ──→ animalPrototype
├─ eat: function()
├─ sleep: function()
└─ [[Prototype]] ──→ Object.prototype
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
// Dog inherits from Animal
function Dog(name, breed) {
Animal.call(this, name); // Inherit own properties (super equivalent)
this.breed = breed;
}
// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix constructor reference
Dog.prototype.bark = function() {
console.log(`${this.name} says: Woof!`);
};
const rex = new Dog("Rex", "Labrador");
rex.eat(); // "Rex is eating" (from Animal)
rex.bark(); // "Rex says: Woof!" (from Dog)
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // trueThis pattern is the foundation of what ES6 classes do automatically.
class Animal {
constructor(name) {
this.name = name;
this.energy = 100;
}
eat() {
this.energy += 10;
console.log(`${this.name} is eating. Energy: ${this.energy}`);
}
sleep() {
this.energy = 100;
console.log(`${this.name} is sleeping. Energy restored.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} says: Woof!`);
}
fetch() {
this.energy -= 5;
console.log(`${this.name} is fetching. Energy: ${this.energy}`);
}
}
class Cat extends Animal {
constructor(name, color) {
super(name);
this.color = color;
}
meow() {
console.log(`${this.name} says: Meow!`);
}
climb() {
this.energy -= 3;
console.log(`${this.name} is climbing. Energy: ${this.energy}`);
}
}
const rex = new Dog("Rex", "Labrador");
const whiskers = new Cat("Whiskers", "Orange");
rex.eat(); // Inherited from Animal
rex.bark(); // Defined in Dog
whiskers.meow(); // Defined in Catsuper serves two purposes in derived classes:
1. super() — Call Parent Constructor
class Employee {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
}
class Manager extends Employee {
constructor(name, salary, department) {
super(name, salary); // MUST be called before using `this`
this.department = department;
}
}Rules for super():
- Must be called in derived class constructors
- Must be called before accessing
this - Can only be used once per constructor
2. super.method() — Call Parent Method
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
describe() {
return `Rectangle: ${this.width} x ${this.height}`;
}
}
class Square extends Rectangle {
constructor(side) {
super(side, side);
}
describe() {
// Call parent method
return `Square (${super.describe()})`;
}
}
const sq = new Square(5);
console.log(sq.area()); // 25 (from Rectangle)
console.log(sq.describe()); // "Square (Rectangle: 5 x 5)"When a child class defines a method with the same name as a parent method, the child's version overrides the parent's.
class Notification {
constructor(message) {
this.message = message;
}
send() {
console.log(`Sending notification: ${this.message}`);
}
format() {
return this.message;
}
}
class EmailNotification extends Notification {
constructor(message, recipient) {
super(message);
this.recipient = recipient;
}
// Override send()
send() {
console.log(`Sending email to ${this.recipient}: ${this.message}`);
}
// Override and extend format()
format() {
const base = super.format();
return `Email: ${base}`;
}
}
class SMSNotification extends Notification {
constructor(message, phoneNumber) {
super(message);
this.phoneNumber = phoneNumber;
}
send() {
console.log(`Sending SMS to ${this.phoneNumber}: ${this.message}`);
}
}
// Polymorphism in action
const notifications = [
new EmailNotification("Hello!", "alice@example.com"),
new SMSNotification("Hello!", "+1234567890")
];
notifications.forEach(n => n.send());
// "Sending email to alice@example.com: Hello!"
// "Sending SMS to +1234567890: Hello!"class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
class Employee extends Person {
constructor(firstName, lastName, id) {
super(firstName, lastName);
this.id = id;
}
get fullName() {
return `[${this.id}] ${super.fullName}`;
}
}
const emp = new Employee("Alice", "Johnson", "E123");
console.log(emp.fullName); // "[E123] Alice Johnson"class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}
const rex = new Dog();
// instanceof checks the entire chain
console.log(rex instanceof Dog); // true
console.log(rex instanceof Mammal); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true
// Getting the prototype chain
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Mammal.prototype); // true
console.log(Object.getPrototypeOf(Mammal.prototype) === Animal.prototype); // true
// isPrototypeOf
console.log(Dog.prototype.isPrototypeOf(rex)); // true
console.log(Mammal.prototype.isPrototypeOf(rex)); // true// ❌ Fragile, rigid hierarchy
class Entity { /* ... */ }
class LivingEntity extends Entity { /* ... */ }
class Animal extends LivingEntity { /* ... */ }
class Mammal extends Animal { /* ... */ }
class Dog extends Mammal { /* ... */ }
class ServiceDog extends Dog { /* ... */ }Problems:
- Tight coupling — changes to a parent affect all children
- Inflexibility — what if a
RobotDogneedsDogbehavior but notMammal? - Duplication — shared behavior across unrelated branches requires copying
// Reusable behavior objects
const CanFly = {
fly() {
console.log(`${this.name} is flying`);
}
};
const CanSwim = {
swim() {
console.log(`${this.name} is swimming`);
}
};
const CanWalk = {
walk() {
console.log(`${this.name} is walking`);
}
};
// Mix behaviors into classes
class Bird {
constructor(name) {
this.name = name;
}
}
Object.assign(Bird.prototype, CanFly, CanWalk);
class Fish {
constructor(name) {
this.name = name;
}
}
Object.assign(Fish.prototype, CanSwim);
class Duck {
constructor(name) {
this.name = name;
}
}
Object.assign(Duck.prototype, CanFly, CanSwim, CanWalk);
const duck = new Duck("Donald");
duck.fly(); // "Donald is flying"
duck.swim(); // "Donald is swimming"
duck.walk(); // "Donald is walking"class CanFly {
fly() {
console.log(`${this.name} is flying at ${this.speed} mph`);
}
}
class CanSwim {
swim() {
console.log(`${this.name} is swimming`);
}
}
class Duck {
name = "Duck";
speed = 50;
flyer = new CanFly();
swimmer = new CanSwim();
fly() {
return this.flyer.fly.call(this);
}
swim() {
return this.swimmer.swim.call(this);
}
}| Use Inheritance When | Use Composition When |
|---|---|
| Clear "is-a" relationship | "has-a" or "can-do" relationship |
| Single, stable hierarchy | Multiple, mixed behaviors |
| Shared interface with variations | Reusable behaviors across unrelated types |
| Framework base classes | Domain models with diverse capabilities |
A mixin is a class or object that provides methods to other classes, without being a parent class. It's a pattern for achieving multiple inheritance-like behavior.
// Mixin factory function
function TimestampMixin(BaseClass) {
return class extends BaseClass {
constructor(...args) {
super(...args);
this.createdAt = new Date();
this.updatedAt = new Date();
}
touch() {
this.updatedAt = new Date();
}
};
}
function SerializableMixin(BaseClass) {
return class extends BaseClass {
toJSON() {
return JSON.stringify(this);
}
fromJSON(json) {
return Object.assign(this, JSON.parse(json));
}
};
}
// Apply mixins
class User {
constructor(name) {
this.name = name;
}
}
const TimestampedUser = TimestampMixin(User);
const SerializableUser = SerializableMixin(TimestampedUser);
const user = new SerializableUser("Alice");
console.log(user.createdAt); // Date object
user.touch();
console.log(user.toJSON()); // JSON stringconst Loggable = {
log(message) {
console.log(`[${this.constructor.name}] ${message}`);
}
};
const Validatable = {
validate() {
return this.isValid !== undefined ? this.isValid : true;
}
};
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
this.isValid = price > 0;
}
}
// Apply mixins
Object.assign(Product.prototype, Loggable, Validatable);
const product = new Product("Widget", 10);
product.log("Created"); // "[Product] Created"
console.log(product.validate()); // trueJavaScript doesn't support multiple class inheritance directly. Here are workarounds:
const MixinA = (Base) => class extends Base { methodA() {} };
const MixinB = (Base) => class extends Base { methodB() {} };
class MyClass extends MixinB(MixinA(Object)) {}class FeatureA { methodA() {} }
class FeatureB { methodB() {} }
class MyClass {
constructor() {
this.a = new FeatureA();
this.b = new FeatureB();
}
}class CanFly {
fly() { throw new Error("Must implement fly()"); }
}
class CanSwim {
swim() { throw new Error("Must implement swim()"); }
}
class Duck extends Animal {
fly() {
console.log("Flying...");
}
swim() {
console.log("Swimming...");
}
}// ❌ Brittle and hard to understand
class Entity { }
class LivingEntity extends Entity { }
class Animal extends LivingEntity { }
class Mammal extends Animal { }
class Canine extends Mammal { }
class Dog extends Canine { }
// ✅ Flatten the hierarchy, use composition
class Dog {
constructor() {
this.movement = new QuadrupedMovement();
this.diet = new CarnivoreDiet();
}
}class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
// ❌ Missing super() — ReferenceError when using `this`
this.breed = breed;
}
}
// ✅ Always call super() first
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}class Animal {
constructor(name) {
super(); // ❌ SyntaxError: 'super' keyword unexpected here
this.name = name;
}
}
// ✅ Only use super() in classes with extends
class Animal {
constructor(name) {
this.name = name;
}
}class Rectangle {
constructor(w, h) {
this.width = w;
this.height = h;
}
setWidth(w) {
this.width = w;
}
setHeight(h) {
this.height = h;
}
area() {
return this.width * this.height;
}
}
// ❌ Violates Liskov Substitution Principle
class Square extends Rectangle {
setWidth(w) {
this.width = w;
this.height = w; // Unexpected side effect!
}
setHeight(h) {
this.width = h;
this.height = h; // Unexpected side effect!
}
}
// ✅ Better: Square is NOT a Rectangle in behavior
class Square {
constructor(side) {
this.side = side;
}
area() {
return this.side ** 2;
}
}// ❌ Using inheritance just to share a utility method
class Utils {
formatDate(date) {
return date.toISOString();
}
}
class User extends Utils { /* ... */ } // Wrong! User is not a Utils!
// ✅ Use a module or object instead
const DateUtils = {
formatDate(date) {
return date.toISOString();
}
};
class User {
constructor() {
this.createdAt = DateUtils.formatDate(new Date());
}
}Create a shape hierarchy with proper inheritance:
class Shape {
// area() — throw error (abstract)
// perimeter() — throw error (abstract)
// describe() — return string description
}
class Rectangle extends Shape {
// Implement area(), perimeter()
}
class Circle extends Shape {
// Implement area(), perimeter()
}
class Triangle extends Shape {
// Implement area(), perimeter()
}
// Test polymorphism
const shapes = [new Rectangle(4, 5), new Circle(3), new Triangle(3, 4, 5)];
shapes.forEach(s => console.log(s.describe()));Build a plugin system using mixins:
function LoggingPlugin(Base) {
return class extends Base {
log(action) {
console.log(`[LOG] ${action} at ${new Date()}`);
}
};
}
function ValidationPlugin(Base) {
return class extends Base {
validate(data) {
return data != null && typeof data === "object";
}
};
}
class Database {
save(data) {
// Should log and validate before saving
}
}
// Apply plugins
const EnhancedDatabase = ValidationPlugin(LoggingPlugin(Database));
const db = new EnhancedDatabase();
db.save({ name: "Alice" }); // Logs and validatesRefactor this inheritance-based code to composition:
class Vehicle {
constructor(speed) { this.speed = speed; }
move() { console.log("Moving"); }
}
class Car extends Vehicle {
honk() { console.log("Beep!"); }
}
class Boat extends Vehicle {
anchor() { console.log("Anchoring"); }
}
class AmphibiousVehicle extends Car {
// Needs both Car AND Boat behavior!
anchor() { console.log("Anchoring"); }
}Trace and predict the output:
class A {
greet() { return "A"; }
}
class B extends A {
greet() { return super.greet() + "B"; }
}
class C extends A {
greet() { return super.greet() + "C"; }
}
class D extends B {
greet() { return super.greet() + "D"; }
}
const d = new D();
console.log(d.greet()); // What does this output?Implement an abstract base class pattern in JavaScript:
class Animal {
constructor() {
if (new.target === Animal) {
throw new Error("Cannot instantiate abstract class");
}
}
speak() {
throw new Error("Must implement speak()");
}
}
class Dog extends Animal {
speak() { return "Woof!"; }
}
class Cat extends Animal {
speak() { return "Meow!"; }
}
const dog = new Dog();
console.log(dog.speak()); // "Woof!"
// const animal = new Animal(); // Should throw- JavaScript uses prototypal inheritance — objects inherit from objects
- Class inheritance (
extends) is syntactic sugar over the prototype chain super()must be called in derived constructors before usingthissuper.method()calls a parent class method from an override- Method overriding lets subclasses provide specialized behavior
- Polymorphism allows treating different objects uniformly through a common interface
- Deep inheritance hierarchies are fragile — prefer flat structures
- Composition builds complex behavior by combining simpler objects
- Mixins are functions/classes that add behavior without inheritance
- JavaScript doesn't support multiple class inheritance — use mixins or composition
- The Liskov Substitution Principle states subclasses should be fully substitutable for their parents
- Only use inheritance for true "is-a" relationships, not for code sharing
Now that you understand inheritance:
- OOP Concepts — review the four pillars with practical examples
- Classes — dive deeper into advanced class patterns
- Prototypes — understand the mechanics under the hood
- System Design — learn how OOP patterns apply to larger architectures
Happy coding! 🚀