JavaScript - Inheritance & Composition

Overview

Estimated time: 35–45 minutes

Inheritance models "is-a" relationships (Dog is an Animal), while composition models "has-a" or "uses" relationships (Service has a Logger). Learn when to use each, how to override methods with super, apply mixins for horizontal reuse, and reason about prototype chains.

Learning Objectives

  • Extend classes, override methods, and call super safely.
  • Build composed objects that delegate to collaborators ("has-a").
  • Apply mixins to add capabilities without deep inheritance.
  • Use instanceof and prototype checks appropriately.

Prerequisites

Class inheritance and super

class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a noise.`; }
}

class Dog extends Animal {
  constructor(name) { super(name); }
  speak() { return `${super.speak()} Woof!`; } // call parent and extend
}

new Dog('Fido').speak(); // "Fido makes a noise. Woof!"

Composition (has-a, uses)

Prefer composition to reuse behavior without tight coupling or deep hierarchies.

class Logger {
  log(msg) { console.log(`[LOG] ${msg}`); }
}

class Service {
  constructor(logger = new Logger()) { this.logger = logger; }
  doWork() { this.logger.log('working'); return true; }
}

new Service().doWork(); // logs and returns true

Mixins (horizontal reuse)

Mixins add capabilities to a class without extending a specific base. You can extend a base class and then mix in features.

const CanEat = Base => class extends Base {
  eat(food) { return `${this.name} eats ${food}`; }
};

const CanSleep = Base => class extends Base {
  sleep() { return `${this.name} sleeps`; }
};

class Creature { constructor(name){ this.name = name; } }
class Cat extends CanSleep(CanEat(Creature)) {
  speak(){ return `${this.name} meows`; }
}

new Cat('Misty').eat('fish'); // "Misty eats fish"

instanceof and prototypes

const a = new Animal('A');
a instanceof Animal;        // true
Animal.prototype.isPrototypeOf(a); // true

// Duck typing alternative: feature detection
function canSpeak(x) { return typeof x?.speak === 'function'; }
canSpeak(new Dog('Fido'));  // true

Common Pitfalls

  • super() first: In derived constructors you must call super() before using this.
  • Private fields: #private fields are not accessible in subclasses or outside the class body.
  • Deep hierarchies: Avoid overuse of inheritance; prefer composition or mixins for cross-cutting capabilities.
  • Multiple inheritance: Not supported; use mixins to combine behaviors.
  • Overriding vs shadowing: Arrow-function instance fields create per-instance methods and can shadow prototype methods (harder to override/test).

Try it

Run to compare inheritance, composition, and mixins: