Mixins
JavaScript classes only support single inheritance — class B extends A and that is the end of the chain. When you want to share a capability across many classes that otherwise have nothing in common, the answer is a mixin: a chunk of behaviour you splice into a class. There are two common ways to write one, and each has its place.
Pattern 1: copying methods onto the prototype
The simplest mixin is a plain object of methods. Object.assign copies them onto a class's prototype, and instances pick them up through normal inheritance.
// A capability — sayHi to anyone with a name
const greeter = {
greet() {
return `Hi, I am ${this.name}`;
},
shout() {
return this.greet().toUpperCase();
},
};
class Person {
constructor(name) { this.name = name; }
}
Object.assign(Person.prototype, greeter);
const p = new Person("Ada");
console.log(p.greet()); // 'Hi, I am Ada'
console.log(p.shout()); // 'HI, I AM ADA'The methods become real members of Person.prototype. instanceof is unaffected because the class chain has not changed.
Pattern 2: class factories
A more powerful pattern: a function that takes a base class and returns a new class that extends it with the mixin's behaviour. You can chain factories to compose multiple capabilities.
// Each mixin is a function: Base => class extends Base { ... }
const Serializable = Base => class extends Base {
toJSON() {
return Object.fromEntries(
Object.entries(this).filter(([k]) => !k.startsWith("_"))
);
}
};
const Timestamped = Base => class extends Base {
createdAt = new Date();
age() { return Date.now() - this.createdAt.getTime(); }
};
// Stack them
class Doc extends Serializable(Timestamped(Object)) {
constructor(title) {
super();
this.title = title;
this._secret = "internal";
}
}
const d = new Doc("hello");
console.log(d.toJSON()); // { createdAt: ..., title: 'hello' }
console.log(d.age() >= 0); // trueEach factory builds a real anonymous class in the prototype chain. Because they extend through super, instance methods compose cleanly and instanceof still works for the outermost class.
Mixins with state
Class-factory mixins can carry their own fields and even constructors — they just have to call super so the chain stays valid.
const Counted = Base => class extends Base {
static instances = 0;
constructor(...args) {
super(...args);
this.constructor.instances++;
}
};
class Widget extends Counted(Object) {}
class Gadget extends Counted(Object) {}
new Widget(); new Widget(); new Gadget();
console.log(Widget.instances); // 2
console.log(Gadget.instances); // 1Naming collisions
Mixins compose — but what happens if two of them define the same method?
In the prototype-copy pattern: later
Object.assignwins. The last mixin overwrites earlier ones silently.In the class-factory pattern: the innermost mixin defines the method first, each outer one can call
super.method()to keep the chain alive.When in doubt, name your mixin methods to avoid collisions, or expose a single well-named entry point.
Mixins vs inheritance vs composition
Use inheritance when one class genuinely is a kind of another and shares its lifecycle and identity.
Use mixins when several unrelated classes need the same capability bolted on.
Use composition when behaviour is owned by a delegated object —
this.logger = new Logger()is often simpler and easier to test than mixing the logger in.Prefer composition when in doubt. Reach for mixins for cross-cutting concerns that would otherwise be duplicated across many classes.
A small real-world flavour
const EventEmitter = Base => class extends Base {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
this.#listeners.get(event).push(fn);
return this;
}
emit(event, ...args) {
for (const fn of this.#listeners.get(event) ?? []) fn(...args);
}
};
class Button extends EventEmitter(Object) {
click() { this.emit("click"); }
}
const btn = new Button();
btn.on("click", () => console.log("clicked!"));
btn.click();clicked!