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 usingthis
. - 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: