Learn JS Series (#43) - Mixins: Sharing Behavior Without a Single Inheritance Line
Learn JS Series (#43) - Mixins: Sharing Behavior Without a Single Inheritance Line
What will I learn
- You will learn what a mixin is: a bundle of behaviour you blend into objects or classes;
- why mixins solve a problem single inheritance simply cannot: sharing capabilities across unrelated types;
- how to write object mixins with
Object.assign; - how to write class mixins as functions that take a class and return a class;
- how the function form lets mixed-in methods live on the prototype chain and even call
super; - the trade-offs of mixins versus inheritance and plain composition, and when NOT to reach for them.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-42 read, especially classes, inheritance,
Object.assign, and the duck-typing idea from last time.
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
- Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
- Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)
- Learn JS Series (#26) - Currying and Partial Application
- Learn JS Series (#27) - Function Composition: Building Pipelines from Small Functions
- Learn JS Series (#28) - Pure Functions and Side Effects: The Foundation of Predictable Code
- Learn JS Series (#29) - Memoization: Trading Memory for Speed with Closures
- Learn JS Series (#30) - Mini Project: A Small Functional Utility Library
- Learn JS Series (#31) - Object Literals, Shorthand, and Computed Property Names
- Learn JS Series (#32) - Property Descriptors: writable, enumerable, configurable
- Learn JS Series (#33) - Getters and Setters: Computed Properties That Look Like Data
- Learn JS Series (#34) - The Prototype: JavaScript's Actual Inheritance Mechanism
- Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works
- Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance
- Learn JS Series (#37) - Constructor Functions and the new Operator, Step by Step
- Learn JS Series (#38) - The class Syntax: Sugar Over Prototypes, and What It Hides
- Learn JS Series (#39) - Class Inheritance: extends, super, and the Prototype Chain Again
- Learn JS Series (#40) - Static Members, Static Blocks, and Class-Level State
- Learn JS Series (#41) - Private Fields (#) and True Encapsulation
- Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly
- Learn JS Series (#43) - Mixins: Sharing Behavior Without a Single Inheritance Line (this post)
Learn JS Series (#43) - Mixins: Sharing Behavior Without a Single Inheritance Line
Last episode we ended on a mindset shift: instead of asking "is this an instance of class X?", duck typing asks "does this object have the capabilities I actually need?". I promised we would pick that thread back up, and here we are. Because once you start thinking in terms of capabilities rather than lineage, you very quickly bump into a practical question: how do I give a capability to several unrelated classes without forcing them all under one shared parent? Single inheritance flat-out cannot express that, and pretending it can is where quite some class hierarchies go to die. The answer is a pattern called a mixin, and it is one of those ideas that looks strange the first time you see it (a function that returns a class -- wait, what?) and then becomes completely obvious the moment it clicks. Having said that, mixins are also easy to overuse, so we will spend just as much time on when NOT to reach for them. Let's get into it. ;-)
Solutions to Episode 42 Exercises
Exercise 1 - a three-level hierarchy:
class Vehicle {}
class Car extends Vehicle {}
class SportsCar extends Car {}
const s = new SportsCar();
console.log(s instanceof SportsCar, s instanceof Car, s instanceof Vehicle); // true true true
console.log(s instanceof Object, s instanceof Array); // true false
The insight: every prototype on the chain (SportsCar, Car, Vehicle, and Object at the top) matches, because instanceof walks the whole chain; Array is not on the chain, so it is false.
Exercise 2 - a type describer:
function describeType(value) {
if (Array.isArray(value)) return "array";
if (value === null) return "null";
return typeof value; // "object", "number", "function", ...
}
console.log(describeType([]), describeType(null), describeType({}), describeType(3), describeType(() => {}));
// "array" "null" "object" "number" "function"
The insight: Array.isArray and an explicit === null cover the two cases plain typeof gets embarrassingly wrong (typeof [] is "object", and typeof null is also "object").
Exercise 3 - exact class checking:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
const d = new Dog();
console.log(d instanceof Animal); // true - but a Cat would be too
console.log(d.constructor === Dog); // true - exactly a Dog, nothing else
The insight: instanceof matches every ancestor, so use constructor (or Object.getPrototypeOf) for the exact class; and Array.isArray beats instanceof Array because it works across realms.
Now let's tackle a technique for sharing behaviour that inheritance alone cannot express.
The problem inheritance cannot solve
Single inheritance (episode 39) has a fundamental limit that trips people up constantly: a class can extend exactly one parent. Not two, not three. One. That is perfectly fine for a clean "is-a" hierarchy -- a SportsCar is a Car is a Vehicle -- but real-world capabilities rarely line up so neatly. Imagine you have three completely unrelated classes -- a Document, a User, and a Product -- and you want all three to be serializable (able to turn themselves into JSON) and timestamped (tracking when they were created). Those are not a shared type. A User is not a kind of Document, and a Product is not a kind of User. What they share is a set of capabilities, and you cannot say "is serializable AND is timestamped AND is a document" with a single line of inheritance.
You can feel the limit directly by trying to write it out. The language will stop you cold:
class Serializable { toJSON() { return JSON.stringify(this); } }
class Timestamped { stamp() { this.at = "2026-08-13"; return this; } }
// class Document extends Serializable, Timestamped {} // ERROR - only ONE parent allowed
class Document extends Serializable {} // you are forced to pick just one
console.log(new Document().toJSON()); // works, but Document can never ALSO be Timestamped this way
There it is. The commented-out line is not valid JavaScript -- extends takes a single expression, full stop. Some languages (C++ being the famous one) allow multiple inheritance, where a class can name several parents, but that opens up its own can of worms (the notorious "diamond problem", where two parents share a grandparent and the engine has to guess which version of a method wins). JavaScript deliberately said "no thanks" and gives you single inheritance only. So we need another mechanism entirely for cross-cutting capabilities.
That mechanism is the mixin. A mixin is a reusable bundle of methods -- a capability -- that you can blend into any object or class, regardless of where it sits in a hierarchy. Instead of "X is a Y", a mixin says "X can also do Z". It is composition of behaviour, not inheritance of type. And it maps beautifully onto the capability-first thinking we met with duck typing last episode: you are literally handing an object the abilities it needs, no lineage required.
Object mixins with Object.assign
The simplest form of mixin works at the object level, and you already know the tool for it: Object.assign (episode 31 touched on it). A mixin here is just a plain object holding some methods, and Object.assign(target, ...sources) copies those methods onto a target object. You can blend several capability-objects into one target in a single call:
const serializable = {
toJSON() { return JSON.stringify(this); },
};
const timestamped = {
stamp() { this.createdAt = "2026-08-13"; return this; },
};
const doc = { title: "notes" };
Object.assign(doc, serializable, timestamped); // blend BOTH capabilities in
doc.stamp();
console.log(doc.toJSON()); // '{"title":"notes","createdAt":"2026-08-13"}'
Object.assign copied the toJSON and stamp methods straight onto doc, so doc now has both capabilities without inheriting from anything at all. To be precise about the mechanics: Object.assign(target, ...sources) copies all enumerable own properties from each source into the target, processing sources left to right (so a later source wins on any key collision), and it returns the target. This is mixing behaviour into one specific object.
The upside of this form is that it is dead simple and requires no ceremony. The downside is subtle but important: the methods are copied as own properties directly onto doc. If you did this to a thousand objects, each one would carry its own copy of toJSON and stamp -- that is a thousand duplicate function objects sitting in memory in stead of one shared version. For a handful of objects, who cares. For many, it matters. That memory concern is exactly what pushes us toward the class-based form.
Class mixins as functions
For classes, the idiomatic mixin is a bit of a party trick at first glance: it is a function that takes a class and returns a new class extending it, with extra methods added. This leans on class expressions (episode 38 showed classes can be values, not just declarations). The mixin function receives a base class, and produces a fresh subclass that carries the mixed-in behaviour:
// a mixin: takes a class, returns an extended class
const Serializable = (Base) => class extends Base {
toJSON() { return JSON.stringify(this); }
};
const Timestamped = (Base) => class extends Base {
stamp() { this.createdAt = "2026-08-13"; return this; }
};
class Model {
constructor(name) { this.name = name; }
}
// apply BOTH mixins by nesting the function calls:
class Document extends Serializable(Timestamped(Model)) {}
const d = new Document("report");
d.stamp();
console.log(d.toJSON()); // '{"name":"report","createdAt":"2026-08-13"}'
console.log(d instanceof Model); // true - still a Model, plus the mixed-in behaviour
Read Serializable(Timestamped(Model)) from the inside out, the same way you would read nested function calls anywhere else (episode 27, function composition, is exactly this shape). Start with Model. Wrap it with Timestamped(...), which returns a brand-new anonymous class that extends Model and adds stamp. Then wrap that with Serializable(...), which returns yet another class extending the previous one and adding toJSON. The result is a little prototype chain -- Document -> Serializable-class -> Timestamped-class -> Model -> Object.prototype -- that Document sits on top of. So d ends up with name, stamp, and toJSON, blended from three separate sources, and it is still instanceof Model because Model really is on its chain. This is capability composition expressed through the class system, and it neatly sidesteps the single-parent limit without any multiple-inheritance headache.
Why the function-returning-class form
So why go through all this ceremony when Object.assign looked so easy? Because the function form has two real advantages over copying methods onto an object or a prototype.
First, memory: because each mixin extends the base, the mixed-in methods live on the prototype chain (episode 35), not on each instance. Ten thousand Document instances share one single toJSON function, exactly like normal class methods. No duplication.
Second -- and this is the one people underestimate -- because the mixin is a genuine subclass, its methods can call super to cooperate with the class they are mixed into. That is something Object.assign-style copying simply cannot do, since a copied method has no super to reach for:
const Loud = (Base) => class extends Base {
describe() {
return super.describe().toUpperCase(); // cooperate with the base's method via super
}
};
class Item { describe() { return "an item"; } }
class LoudItem extends Loud(Item) {}
console.log(new LoudItem().describe()); // "AN ITEM" - the mixin WRAPPED the base's method
Look at what happened there. Loud did not replace describe, it decorated it: it called super.describe() to get the base's answer ("an item") and then transformed it. Because Loud(Item) produces a real class that extends Item, super inside the mixin points straight at Item.prototype.describe. This "wrap and enhance" ability is genuinely powerful -- it is how you layer behaviour like logging, validation, or formatting on top of existing methods without touching the original class. The pattern looks unusual (a function that returns a class), but it is nothing more than first-class functions (episode 15) plus class expressions (episode 38), two things you already know, clicking together.
A more realistic example
Let me show you why this matters with something closer to real code. Say you are building a small app and several unrelated classes need to emit events -- a Button in the UI, a Timer, a Socket. They have nothing in common as types, but they all want the same capability: "let other code subscribe to my events". That is a textbook mixin.
const Emitter = (Base) => class extends Base {
#listeners = {};
on(event, fn) {
(this.#listeners[event] ||= []).push(fn); // register a listener
return this;
}
emit(event, ...args) {
for (const fn of this.#listeners[event] || []) fn(...args); // fire them all
return this;
}
};
class Timer {}
class Button {}
class EventfulTimer extends Emitter(Timer) {}
class EventfulButton extends Emitter(Button) {}
const t = new EventfulTimer();
t.on("tick", (n) => console.log(`tick ${n}`));
t.emit("tick", 1); // "tick 1"
const b = new EventfulButton();
b.on("click", () => console.log("clicked!"));
b.emit("click"); // "clicked!"
One mixin, Emitter, gave the exact same event capability to two classes that share no common ancestor beyond Object. Notice it even carries its own private #listeners field (episode 41), so each instance keeps its listeners properly encapsulated. This is the sweet spot for mixins: a genuine, self-contained capability that quite some unrelated types all want. Try expressing that with single inheritance and you will end up either duplicating the code or bolting on some awkward artificial EventEmitterBase that everything is forced to descend from.
A quick look sideways: mixins in Python
Many of you came to this series from the Learn Python Series, so a short comparison sharpens what JavaScript is doing here. Python takes the road JavaScript refused: it supports genuine multiple inheritance, and "mixin classes" are an everyday, idiomatic pattern. You simply list several base classes, and Python's method resolution order (the famous MRO) decides which method wins:
class Serializable:
def to_json(self):
import json
return json.dumps(self.__dict__)
class Timestamped:
def stamp(self):
self.created_at = "2026-08-13"
return self
class Document(Serializable, Timestamped): # list BOTH parents - legal in Python
def __init__(self, name):
self.name = name
d = Document("report")
d.stamp()
print(d.to_json()) # {"name": "report", "created_at": "2026-08-13"}
That class Document(Serializable, Timestamped) line is precisely the thing JavaScript rejected as a SyntaxError earlier. Python allows it and resolves conflicts through the MRO. So the JavaScript "function that returns a class" trick is really our workaround for not having multiple inheritance -- we rebuild the same capability-blending behaviour out of single inheritance plus first-class functions. Neither approach is strictly better; Python's is more direct but the MRO can get genuinely confusing in deep hierarchies, while JavaScript's is more verbose but every step is explicit and there is no hidden resolution order to reason about. Same goal, different route -- worth knowing if you hop between the two languages.
The trade-offs
Mixins are powerful, and part of using them well is knowing when to leave them in the drawer. Let me lay out both sides honestly, because I have seen quite some codebases suffer from mixin overload.
The upsides are clear: they let unrelated classes share capabilities without contorting a hierarchy, and they favour "has-a-capability" composition over rigid "is-a" inheritance -- which tends to age better as requirements change.
The downsides are just as real. First, silent clashes: if two mixins both define toJSON differently, the last one applied quietly wins and the other vanishes with no warning whatsoever. Second, lost provenance: when a reader sees d.stamp(), where did stamp come from? With deep mixin stacks the origin of a method becomes a scavenger hunt. Third, stacking depth: five nested mixin calls can be every bit as hard to follow as a five-level inheritance chain -- you traded one kind of complexity for another. Because of all this, a lot of modern JavaScript leans away from mixins and toward plain composition: give an object a property that holds a capability-object, and delegate to it explicitly, rather than blending methods in.
// plain composition alternative: HAS-A a capability, rather than mixing it in
class Logger {
log(msg) { return `[log] ${msg}`; }
}
class Service {
constructor() { this.logger = new Logger(); } // has-a logger
run() { return this.logger.log("running"); } // delegate to it explicitly
}
console.log(new Service().run()); // "[log] running"
Notice how in the composition version there is zero mystery about where log lives -- it is right there on this.logger, and run calls it out loud. Each capability stays in its own object, and the relationships are explicit rather than flattened together. We explore this functional, composition-first style in much more depth in Phase 10, but the seed of it is worth planting now: when the relationship is genuinely "has-a", plain composition usually reads clearer than a mixin.
So my honest guidance: use mixins for genuine cross-cutting capabilities shared across unrelated types (the Emitter example is a perfect fit), keep each mixin small and non-conflicting, and prefer plain composition when the relationship is really "has-a". Both are valid tools. As always, the winning move is to pick whichever makes the code easiest for the next person to read -- and that next person is very often you, six months from now, having forgotten every clever thing you did today. ;-)
Try it yourself
- Create two object mixins,
comparable(with anequals(other)method comparing anid) andprintable(with aprint()method that returns a string), and useObject.assignto blend both into a plain object that has anidand aname. Call both mixed-in methods and log the results. - Write a class mixin
Countableas a function that takes a base class and returns a subclass adding anincrement()method backed by acountproperty (start it at zero). Apply it to a simple base class, create an instance, and increment it a few times, logging the count each time. - Stack two class mixins onto a base class (nesting the function calls), create an instance, and confirm with
instanceofthat it has methods from both mixins AND is still an instance of the base class. Then rewrite the same capability using plain composition (a has-a property you delegate to) and explain, in one sentence each, one advantage of the mixin version and one advantage of the composition version.
So what did we actually cover?
- A mixin is a reusable bundle of behaviour (a capability) blended into objects or classes, expressing "can do Z" rather than "is a Y" -- the capability-first thinking from last episode, made concrete.
- Mixins solve what single inheritance cannot: sharing capabilities across unrelated types, because a class can
extendonly one parent. - Object mixins use
Object.assign(target, ...sources)to copy methods onto a specific object -- simple, but each object carries its own copy. - Class mixins are functions that take a base class and return an extended class; nest the calls to stack several. The methods live on the prototype chain (shared across instances) and can call
superto wrap the base's behaviour. - Trade-offs: mixins can clash silently, obscure where methods come from, and stack into hard-to-follow depths -- keep them small and non-conflicting.
- Plain composition (a has-a property you delegate to) is often clearer when the relationship is really "has-a"; choose whichever reads best.
Next episode we circle back to a persistent source of class bugs that has been lurking behind several of these examples: how this behaves inside class methods, why it silently gets lost the moment a method is detached from its object, and the clean fixes for it. It ties directly back to the five this rules from episode 22.