The limits of single inheritance
JavaScript classes support only single inheritance: a class can extend exactly one
parent. That's fine for strict "is-a" hierarchies, but real software often needs to share
capabilities across unrelated types. A Robot and a Car might both need
"serializable," a User and an Order might both need "timestamped" — yet they don't share
a common ancestor. Forcing them into one inheritance tree leads to the infamous deep, brittle
hierarchies and "god base classes." Mixins and composition solve this by letting you
combine independent behaviors without a single rigid lineage.
The guiding principle, often summarized as "favor composition over inheritance," is that building objects from small, focused pieces is more flexible than carving an ever-deeper class tree.
What is a mixin?
A mixin is a reusable bundle of methods that you mix into a class or object, independent
of the inheritance chain. The simplest form copies methods onto a prototype with
Object.assign.
const serializable = {
toJSON() { return JSON.stringify(this) },
fromJSON(s) { return Object.assign(this, JSON.parse(s)) }
}
class User {
constructor(name) { this.name = name }
}
Object.assign(User.prototype, serializable) // mix in the behavior
new User('Ada').toJSON() // '{"name":"Ada"}'
Now any User instance has toJSON, even though User doesn't extend anything. The same
serializable object can be mixed into completely unrelated classes.
Object-level mixins
You can also mix capabilities directly into a single object rather than a whole class — handy for one-off composition.
const comparable = {
equals(other) { return this.id === other.id }
}
const loggable = {
log() { console.log(`[${this.constructor.name}] ${this.id}`) }
}
const entity = Object.assign({ id: 1 }, comparable, loggable)
entity.equals({ id: 1 }) // true
Each source's methods are copied in; later sources override earlier ones on key conflicts — the same "last wins" rule as object spread.
Functional mixins (class factories)
The most powerful mixin pattern is a function that takes a base class and returns a new
subclass extending it. This composes cleanly and supports super.
const Timestamped = (Base) => class extends Base {
constructor(...args) {
super(...args)
this.createdAt = Date.now()
}
touch() { this.updatedAt = Date.now() }
}
const Serializable = (Base) => class extends Base {
serialize() { return JSON.stringify(this) }
}
class Model {}
class Document extends Serializable(Timestamped(Model)) {} // stack mixins
const doc = new Document()
doc.createdAt // a timestamp
doc.serialize() // works too
This "class factory" approach is more robust than Object.assign because each mixin becomes
a real link in the prototype chain — so super works, instanceof-style checks behave, and
ordering is explicit. Libraries and frameworks favor this pattern.
Why mixins beat deep hierarchies
Consider modeling abilities like flying, swimming, and walking across animals. With
single inheritance you'd contort the tree (FlyingSwimmingAnimal?). With mixins each ability
is an independent piece you compose as needed.
const CanFly = (B) => class extends B { fly() { return 'flying' } }
const CanSwim = (B) => class extends B { swim() { return 'swimming'} }
class Animal {}
class Duck extends CanFly(CanSwim(Animal)) {} // both abilities, no awkward tree
class Fish extends CanSwim(Animal) {} // just one
new Duck().fly() // 'flying'
new Duck().swim() // 'swimming'
Each capability is defined once and combined à la carte. Adding a new ability doesn't require reshaping a hierarchy — you just write another mixin.
Composition with plain objects (delegation)
Beyond mixins, pure composition builds an object's behavior by holding other objects and delegating to them, rather than inheriting at all.
function createPlayer(name) {
const health = createHealth(100) // composed pieces
const inventory = createInventory()
return {
name,
takeDamage: health.decrease, // delegate
addItem: inventory.add,
}
}
This "has-a" style keeps each piece independently testable and swappable. It avoids the prototype chain entirely and is often the most flexible approach for complex domain objects.
Pitfalls of mixins
Mixins are powerful but have sharp edges to respect:
// name collisions — a later mixin silently overwrites an earlier method
Object.assign(proto, mixinA, mixinB) // if both define save(), B wins silently
- Name collisions: two mixins defining the same method clobber each other quietly. Keep mixin method names specific, or detect collisions in development.
- Hidden dependencies: a mixin that assumes
this.idorthis.save()exists couples it to its host invisibly. Document what each mixin requires. - Order sensitivity: with class-factory mixins, the stacking order affects
superchains. Be deliberate about sequence.
These aren't reasons to avoid mixins — just to use focused, well-named, self-contained ones.
Choosing your tool
A quick decision guide:
extends— a genuine, stable "is-a" relationship with one clear parent.- Mixins — orthogonal capabilities shared across unrelated classes (serializable, observable, timestamped).
- Composition/delegation — complex objects assembled from independent, swappable parts; when you want maximum flexibility and testability.
Most well-designed systems use all three: shallow inheritance for the core type, mixins for cross-cutting traits, and composition for assembling behavior. Reaching first for deep inheritance is the usual mistake.
Key takeaways
- JavaScript allows only single inheritance; mixins and composition share behavior across unrelated types.
- A mixin is a reusable method bundle, applied via
Object.assign(flat copy) or as a class factory(Base) => class extends Base {...}(real prototype link,super-aware). - Class-factory mixins stack cleanly to compose multiple independent capabilities.
- Pure composition (has-a, delegation) often beats inheritance for complex, evolving objects.
- Watch for mixin pitfalls: name collisions, hidden
thisdependencies, and order sensitivity. - Favor composition over deep inheritance; combine
extends, mixins, and composition by purpose.
Mastering these patterns frees you from forcing every relationship into a single class tree — the hallmark of flexible, maintainable object-oriented JavaScript.