Mixins & composition
Share behavior across unrelated JavaScript classes with mixin objects, class mixin functions, trait-style conflict checks, composition, and a small event mixin.
- 01Copy a mixin object safelyUse
Object.assigndeliberately and spot overwrite and getter pitfalls. - 02Stack class mixin functionsRead the generated prototype chain and predict
supermethod order. - 03Choose composition when it is clearerCompare mixins, traits, inheritance, and object factories for real code.
Share abilities without forcing a family tree
Classes are great when one thing truly is a special kind of another. But real programs often have abilities that cross family lines. A User, an Order, and an Invoice might all know how to serialize themselves. None of them is a kind of the others.
A mixin shares a small ability across unrelated classes. Instead of making every class inherit from one giant base class, you apply only the ability it needs. This lesson goes deeper than the earlier Delegation vs classical inheritance lesson: we will use classes, prototypes, super, conflict checks, and a live event mixin.
Imagine a sheet of stickers that say what an object can do: can serialize, can validate, can emit events. You can put the same sticker on many unrelated things. The sticker does not make a bottle become a laptop; it just adds one ability.
- In real life: A waterproof sticker
- In JavaScript: A
CanSerializemixin withtoJSON() - In real life: Put it on a laptop, bottle, or notebook
- In JavaScript: Apply it to
User,Order, orInvoice - In real life: Two stickers with the same label cover each other
- In JavaScript: Two methods with the same name clash
Where the analogy stops: Real stickers are visual and harmless. JavaScript mixins change real objects or generated classes, so a name clash can replace behavior unless you guard against it.
Composition says: build the thing from parts it owns or delegates to, instead of asking which ancestor it belongs under. It is often the simplest way to avoid inheritance tangles.
- In real life: A desk built from legs, a top, and drawers
- In JavaScript: An object built from a validator, store, and event hub
- In real life: Swap metal legs for wooden legs
- In JavaScript: Pass a different collaborator in tests
- In real life: No family tree is needed
- In JavaScript: No
extendschain is required
Where the analogy stops: Objects still need clear interfaces. Composition is not an excuse to throw random pieces together without naming what each part promises.
We will use five tools in order: mixin objects, class mixin functions, traits, composition, and an event mixin. Later lessons named Observer & pub/sub and Build an event emitter go much deeper on event architecture; here, events are a practical example of reusable behavior.
Mixin objects: copy methods onto a prototype
STEP THROUGHThe smallest JavaScript mixin is a plain object full of methods. You copy those methods onto a class prototype with Object.assign. Because prototypes are shared, every existing and future instance can find the copied methods through normal method lookup.
This leans on the shallow-copy behavior from the Objects & references lesson: Object.assign copies own enumerable properties. It does not deep-copy hidden state, and it does not ask whether replacing a property is safe.
Predict which describe wins, then step through the copies. This replay records real Object.assign behavior, including the silent overwrite and flattened getter.
script
toJSON() { return { type: this.type, id: this.id }; }};const Labels = { describe() { return "mixin label for " + this.id; }};const GetterPack = { get createdLabel() { return "copied once"; }};class User { constructor(id) { this.type = "user"; this.id = id; } describe() { return "user " + this.id; }}Object.assign(User.prototype, Serializable);Object.assign(User.prototype, Labels);Object.assign(User.prototype, GetterPack);const ada = new User("ada");console.log(ada.describe());console.log(JSON.stringify(ada.toJSON()));console.log(typeof Object.getOwnPropertyDescriptor(User.prototype, "createdLabel").get);console.log(ada.createdLabel);- Name clashes overwrite silently. If the prototype already has
describeand the mixin also hasdescribe, the later assignment wins. - Getters are flattened by
Object.assign. The getter runs while copying, and the returned value is assigned. UseObject.getOwnPropertyDescriptorsplusObject.definePropertieswhen descriptors matter.
Mixin objects are best for small, independent abilities. If a method needs to call super, affect construction, or cooperate with another wrapper, reach for a class mixin function instead.
Class mixin functions: wrapping paper for classes
INTERACTIVEA class mixin function receives a base class and returns a new subclass. It is wrapping paper: put any class inside, and the outside class adds powers while still using normal extends, method lookup, and super.
const Serializable = (Base) => class Serializable extends Base {
toJSON() { return { ...this }; }
};
class Order extends Serializable(BaseRecord) {}Stacking is just function calls around a class: Serializable(Validatable(Base)). The wrapper nearest the final class is searched first. If each wrapper calls super.method(), the call travels down the prototype chain and each layer can add its part on the way back.
Change the stacking order, predict the method resolution order, then step through the actual prototype chain.
script
constructor(id) { this.id = id; } label() { return "record " + this.id; } describe() { return this.label(); }}const Auditable = (Base) => class Auditable extends Base { describe() { return super.describe() + " + audit"; } audit() { return "checked " + this.id; }};const Serializable = (Base) => class Serializable extends Base { describe() { return super.describe() + " + serial"; } toJSON() { return { id: this.id, audit: this.audit() }; }};class Order extends Serializable(Auditable(BaseRecord)) {}const order = new Order("A17");console.log(order.describe());console.log(JSON.stringify(order.toJSON()));console.log(Object.getPrototypeOf(Order.prototype).constructor.name);console.log(Object.getPrototypeOf(Object.getPrototypeOf(Order.prototype)).constructor.name);console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Order.prototype))).constructor.name);The generated classes are real classes. The replay uses Object.getPrototypeOf(Order.prototype) to reveal their constructor names. That is why changing the wrapper order changes the chain and changes the final describe() text.
Traits: mixins with rules about conflicts
GUARDED COPYJavaScript does not have a built-in trait keyword. When JavaScript developers say “trait”, they usually mean a mixin-like ability with stricter rules: conflicts must be resolved deliberately instead of overwritten by accident.
function applyTraits(target, ...traits) { for (const trait of traits) { for (const key of Reflect.ownKeys(trait)) { if (key in target) throw new Error("Trait conflict: " + String(key)); Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(trait, key)); } }}const CanTag = { tag() { return "tag:" + this.id; } };const CanLabel = { label() { return "label:" + this.id; } };class Photo { constructor(id) { this.id = id; } }applyTraits(Photo.prototype, CanTag, CanLabel);console.log(new Photo("p1").tag());applyTraits(Photo.prototype, { tag() { return "again"; } });The important line is the guard: if (key in target) throw .... A production trait helper might let you rename, exclude, or explicitly choose a winner. The point is the same: conflicts are part of the API, not a surprise hidden in copy order.
Notice the helper uses Object.getOwnPropertyDescriptor and Object.defineProperty. That copies descriptor shape, so accessors stay accessors. It also lets your helper inspect flags such as enumerable and writable.
Composition over inheritance: build from parts
COMPARE“Composition over inheritance” does not mean inheritance is banned. It means: prefer small parts that collaborate unless a true subtype relationship is obvious and stable. Mixins themselves are a kind of composition at the class level, but object factories can make the parts even more explicit.
function createSerializable(model) { return { toJSON() { return { ...model.data }; } };}function createEventHub() { const listeners = new Set(); return { on(listener) { listeners.add(listener); }, emit(value) { for (const listener of listeners) listener(value); } };}function createOrder(id) { const model = { data: { id, status: "new" } }; return { ...model, ...createSerializable(model), events: createEventHub() };}const order = createOrder("A17");order.events.on((status) => { order.data.status = status; });order.events.emit("paid");console.log(JSON.stringify(order.toJSON()));Here, the order owns plain data, a serializing part, and an event hub. No class needs to pretend it is a child of a SerializableEventedBase. In tests, you can swap the event hub or serializer without rebuilding a class hierarchy.
| Pattern | What it adds | Best when | Watch out for |
|---|---|---|---|
| Mixin object | Copies methods or values onto a prototype | A small ability should be shared by unrelated classes | Name clashes overwrite silently; Object.assign flattens getters |
| Class mixin function | Returns a subclass that extends any base | The ability needs super, construction, or cooperating methods | Stack order becomes method resolution order |
| Trait | A mixin with explicit conflict rules | You want copying, but not silent overwrites | You must define the rules yourself in JavaScript |
| Composition | Builds an object from parts it owns or delegates to | Stateful services or independent parts are clearer than ancestry | There is no automatic super; you wire calls yourself |
When you are choosing a design, sort the situation first.
- Users and Orders both need a
toJSON()helper, but neither is a kind of the other. AdminUsertruly is a specializedUserand should reuseUserconstructor behavior.- An Order needs logging, validation, and a payment gateway that can be swapped in tests.
- Cards, dialogs, and images all need a tiny
enableDrag()ability. - A custom collection wants to behave like an Array and pass Array checks.
- A dashboard owns a notifier, a store, and a renderer that can each be replaced.
Choose the design pressure that fits each situation. Wrong answers explain the trade-off.
An event mixin: on, off, emit
LIVE PLAYGROUNDAn event mixin is a practical ability sticker: many unrelated classes may need on, off, and emit. The methods store listeners on this, so each instance gets its own listener map while sharing the same prototype methods.
const EventMixin = { on(name, listener) { this._events ??= new Map(); const listeners = this._events.get(name) ?? new Set(); listeners.add(listener); this._events.set(name, listeners); return () => this.off(name, listener); }, off(name, listener) { this._events?.get(name)?.delete(listener); }, emit(name, payload) { for (const listener of this._events?.get(name) ?? []) listener(payload); }};class Download { constructor(file) { this.file = file; } }Object.assign(Download.prototype, EventMixin);const download = new Download("mixins.pdf");const unsubscribe = download.on("done", (file) => console.log("saved " + file));download.emit("done", download.file);unsubscribe();download.emit("done", "nothing logs");download.file = "mixins.pdf"done0- No calls yet.
No listeners are subscribed. Click a Subscribe button, then emit done.
on, off, and emit methods shown in the source. Reset creates a fresh Download instance with an empty listener map.The playground calls real listener functions. Subscribe both listeners, emit done, unsubscribe one, and emit again. You are not simulating output cards; you are exercising the same on, off, and emit methods shown beside the buttons.
This is enough to learn mixins. Later, Observer & pub/sub explains event design, and Build an event emitter turns this tiny idea into a fuller utility.
Where you will use this
Mixins show up whenever an ability cuts across a tidy inheritance tree: serialization, validation, event emitting, logging, caching, drag behavior, permission checks, or test helpers. The professional habit is not “use mixins everywhere”; it is “share the smallest honest ability”.
- Use a mixin object for two or three stateless methods that can be copied as-is.
- Use a class mixin function when the ability needs
super, wraps a base class, or should be stacked. - Use a trait helper when accidental overwrites are dangerous and conflicts should fail loudly.
- Use composition when the behavior has its own state or should be swapped independently.
Link only when the lesson exists: for earlier background, revisit Delegation vs classical inheritance and Objects & references.
Common misconceptions
“Mixins are just inheritance with a different name.”
A class mixin function creates subclasses, but the design goal is different: share a focused ability across unrelated classes instead of forcing every class under one ancestor.
“Object.assign is always a safe way to mix in behavior.”
It is convenient, not safe. It overwrites names silently and reads getters instead of preserving them.
“Traits are a separate JavaScript feature.”
JavaScript has no built-in trait syntax. You implement trait-style rules with helper functions and descriptors.
“Composition means no classes.”
Composition means building from parts. Those parts can be plain objects, functions, class instances, or mixin-generated classes.
“An event mixin is a full event system.”
The small mixin teaches reusable methods and instance state. Real event systems add once-only listeners, errors, async behavior, ordering guarantees, and cleanup policies.
Practice: mixins and composition
5 EXERCISESRun the code and enter the exact line printed.
const CanArchive = {
archive() { return this.id + ":archived"; }
};
class Ticket { constructor(id) { this.id = id; } }
Object.assign(Ticket.prototype, CanArchive);
console.log(new Ticket("T1").archive());const CanArchive = {
archive() { return this.id + ":archived"; }
};
class Ticket { constructor(id) { this.id = id; } }
Object.assign(Ticket.prototype, CanArchive);
console.log(new Ticket("T1").archive());Object.assign puts archive on Ticket.prototype. The new instance finds it through the prototype and this.id is T1, so the output is T1:archived.
Without running it first, predict the printed string.
class Base { name() { return "Base"; } }
const A = (BaseClass) => class A extends BaseClass {
name() { return super.name() + "->A"; }
};
const B = (BaseClass) => class B extends BaseClass {
name() { return super.name() + "->B"; }
};
class Final extends B(A(Base)) {}
console.log(new Final().name());Lookup starts at B, B calls A with super, and A calls Base. Base returns Base; A appends ->A; B appends ->B.
Use a safe helper so the clash is caught instead of overwritten. What message proves it?
function safeAssign(target, source) {
for (const key of Reflect.ownKeys(source)) {
if (key in target) throw new Error("Conflict: " + String(key));
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
}
}
const CanName = { name() { return "mixin"; } };
class Person { name() { return "person"; } }
try { safeAssign(Person.prototype, CanName); } catch (error) { console.log(error.message); }function safeAssign(target, source) {
for (const key of Reflect.ownKeys(source)) {
if (key in target) throw new Error("Conflict: " + String(key));
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
}
}
const CanName = { name() { return "mixin"; } };
class Person { name() { return "person"; } }
try { safeAssign(Person.prototype, CanName); } catch (error) { console.log(error.message); }The helper sees name already exists on Person.prototype and throws Conflict: name before anything is overwritten.
Write a class mixin function that adds a label() method.
class Base { constructor(name) { this.name = name; } }
const Named = (BaseClass) => class Named extends BaseClass {
label() { return "item:" + this.name; }
};
class Product extends Named(Base) {}
console.log(new Product("book").label());class Base { constructor(name) { this.name = name; } }
const Named = (BaseClass) => class Named extends BaseClass {
label() { return "item:" + this.name; }
};
class Product extends Named(Base) {}
console.log(new Product("book").label());Named(Base) returns a subclass with label. Product extends that generated class, so instances can call label().
Complete the event ability, apply it to Timer, and check the log.
const EventMixin = {
on(name, listener) { this.events ??= {}; (this.events[name] ??= []).push(listener); },
emit(name, value) { for (const listener of this.events?.[name] ?? []) listener(value); }
};
class Timer {}
Object.assign(Timer.prototype, EventMixin);
const timer = new Timer();
timer.on("tick", (value) => console.log("tick " + value));
timer.emit("tick", 1);const EventMixin = {
on(name, listener) { this.events ??= {}; (this.events[name] ??= []).push(listener); },
emit(name, value) { for (const listener of this.events?.[name] ?? []) listener(value); }
};
class Timer {}
Object.assign(Timer.prototype, EventMixin);
const timer = new Timer();
timer.on("tick", (value) => console.log("tick " + value));
timer.emit("tick", 1);The mixin stores a listener under tick. After copying it to Timer.prototype, timer.emit("tick", 1) calls that listener and logs tick 1.
Quiz: check your understanding
7 QUESTIONSEvery choice explains the trade-off, so read the feedback even when you are right.
Question 1 of 7What is a mixin object?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst M = { speak() { return "mixin"; } }; class Pet { speak() { return "pet"; } } Object.assign(Pet.prototype, M); console.log(new Pet().speak());Choose an answer to see the explanation.
Question 3 of 7What happens to a getter copied with
Object.assign?Choose an answer to see the explanation.
Question 4 of 7What does this class mixin stack print?
Read the code, then predictclass Base { name() { return "Base"; } } const A = (BaseClass) => class A extends BaseClass { name() { return super.name() + " A"; } }; const B = (BaseClass) => class B extends BaseClass { name() { return super.name() + " B"; } }; class Final extends B(A(Base)) {} console.log(new Final().name());Choose an answer to see the explanation.
Question 5 of 7Why use a trait helper instead of plain
Object.assign?Choose an answer to see the explanation.
Question 6 of 7Which is composition over inheritance?
Choose an answer to see the explanation.
Question 7 of 7What does this event mixin snippet print?
Read the code, then predictconst EventMixin = { on(name, listener) { this.events ??= {}; (this.events[name] ??= []).push(listener); }, emit(name, value) { for (const listener of this.events?.[name] ?? []) listener(value); } }; class Job {} Object.assign(Job.prototype, EventMixin); const job = new Job(); job.on("done", (value) => console.log("done " + value)); job.emit("done", "A");Choose an answer to see the explanation.
Key takeaways
- Mixin objects are ability sticker packs copied onto prototypes or objects.
Object.assignis shallow, overwrites silently, and flattens getters.- Class mixin functions return subclasses, so they can use
superand stack in a visible prototype chain. - Traits are mixins with conflict rules you define deliberately.
- Composition builds from parts and is often clearer than adding ancestors.
- An event mixin can share
on,off, andemitacross unrelated classes.
Remember the one-liner.
A mixin is a reusable ability you apply to a class or object; composition is choosing small parts over a fragile family tree.
Up next: OOP principles in JavaScript.