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.assignto copy methods into a prototype, and spot when it replaces a method or turns a getter into a plain value. - 02Wrap class mixin functionsRead the chain of classes they make, and guess the order of
supercalls. - 03Choose composition when it is simplerCompare mixins, traits,
extends, and objects built from parts, and pick the one that fits.
Share one ability with classes that aren't related
A class can extend another class with extends. That works well when one thing really is a special kind of another. For example, an AdminUser is a kind of User.
Sometimes, though, very different classes need the same small skill. Say a User, an Order, and an Invoice all need a toJSON() method. That method turns the object into plain data you can save or send. But an order is not a kind of user, and an invoice is not a kind of order. So extends doesn't fit.
A mixin solves this. A mixin is one small ability, such as toJSON(), that you add to a class or an object. You add only the ability the class needs. You don't make every class extend one big parent class.
Picture a sheet of stickers. Each sticker names one thing an object can do: "can save", "can check itself", "can send messages". You can put the same sticker on a laptop, a bottle, and a notebook. The sticker doesn't turn the bottle into a laptop. It only 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
There is a second way to share abilities, called composition. With composition, you build an object out of smaller parts, and the object uses those parts. You don't ask which parent class it should extend.
A desk is made from legs, a top, and drawers. If you want wooden legs instead of metal ones, you swap just the legs. Nobody asks who the desk's parents are.
- In real life: A desk built from legs, a top, and drawers
- In JavaScript: An object built from parts, like its data and an event hub
- In real life: Swap metal legs for wooden legs
- In JavaScript: Pass in a different part, for example in a test
- In real life: Nobody asks about the desk's parents
- In JavaScript: No
extendsis needed
This lesson shows five ways to share an ability, in this order:
- Mixin objects: copy methods from a plain object.
- Class mixin functions: a function that wraps a class in a new class.
- Traits: mixins that stop with an error when two names clash.
- Composition: build an object from parts.
- An event mixin: a real, useful ability with
on,off, andemit.
It builds on the earlier Delegation vs classical inheritance lesson. Events get their own lessons later (Observer & pub/sub and Build an event emitter). Here they are just one example of an ability you can share.
Mixin objects: copy methods onto a prototype
STEP THROUGHThe simplest mixin is a mixin object: a plain object that holds a few methods. You copy those methods into a class's prototype with Object.assign(target, source). Object.assign copies the properties of source into target.
Why the prototype? Every class has one object called its prototype, such as User.prototype. When a user object doesn't have a method itself, JavaScript looks for it there. So a method copied into User.prototype can be used by every user, old and new.
Object.assign copies toJSON() from the mixin object into User.prototype. Now every User can call it, next to its own describe().Object.assign doesn't copy everything. It copies only the source's own enumerable properties:
- Own means the property sits on the source object itself. A property the source only finds on its own prototype is not own.
- Enumerable means the property shows up when you list the object's keys. Most properties are enumerable. You can hide one with
Object.defineProperty.
const inherited = { inherited() { return "not copied"; } };
const source = Object.create(inherited);
source.own = "copied";
Object.defineProperty(source, "hidden", { value: "not copied", enumerable: false });
const target = {};
Object.assign(target, source);
console.log(target.own);
console.log(typeof target.inherited);
console.log(target.hidden);- Line 1 makes an object with an
inherited()method. - Line 2 makes
source, with that object as its prototype. Sosourcecan useinherited(), but the method is not its own. - Line 3 gives
sourceits own property,own. - Line 4 adds a property called
hiddenand setsenumerable: false, so it won't be listed. - Lines 5 and 6 make an empty
targetand copysourceinto it. - Line 7 prints
copied. Lines 8 and 9 printundefined: the inherited method and the hidden property were skipped.
So put a mixin's methods directly on the mixin object. Don't leave them on its prototype, or they won't be copied.
Object.assign can copy several sources in one call. What if two of them have a method with the same name?
const First = { describe() { return "first"; } };
const Second = { describe() { return "second"; } };
const card = {};
Object.assign(card, First, Second);
console.log(card.describe());Lines 1 and 2 make two mixin objects. Both have a method called describe. Line 3 makes an empty card. Line 4 copies First, then Second, into the card. Line 5 prints second. The second describe took the place of the first one, and JavaScript gave no warning.
describe. Second is copied last, so its describe takes the place of the first one, with no warning.The next example puts these ideas together. A User class gets three mixin objects. One of them has a getter. A getter is a property that runs a function each time you read it, written as get name() { ... }. Step through and watch what happens to the getter and to describe.
Guess which describe wins, then step through the copies. Each step shows what Object.assign really did, including the replaced method and the getter that became a plain value.
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);- A copy can replace a method with no warning. If the prototype already has
describeand the mixin also hasdescribe, the one copied last wins. Object.assignturns a getter into a plain value. It reads the getter while copying, so the getter runs once. Only the value it returns is copied. To keep the getter, useObject.getOwnPropertyDescriptorswithObject.defineProperties, as shown next.
Every property has a descriptor: a small object that describes it. For a getter, the descriptor holds the getter function under the name get. Copy the descriptor, and you copy the getter itself, not its value.
const source = {
get label() { return "fresh"; }
};
const target = {};
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
console.log(Object.getOwnPropertyDescriptor(target, "label").get !== undefined);
console.log(target.label);- Lines 1 to 3 make
sourcewith a getter calledlabelthat returns"fresh". - Line 4 makes an empty
target. - Line 5 reads every descriptor of
sourceand puts them ontarget. - Line 6 prints
true: the copy still has agetfunction. - Line 7 reads
target.label. The getter runs and givesfresh.
Copying descriptors keeps getters working. But it still doesn't check for two sources with the same name. The traits section below adds that check.
Mixin objects work best for small methods that stand on their own. A copied method can't use super to call the parent class's version of a method. When you need super, use a class mixin function instead.
Class mixin functions: a function that returns a class
INTERACTIVEA class mixin function is a function that takes a class and returns a new class. The new class extends the one you passed in and adds one ability. Because the result is a real class, its methods can use super to call the parent class's version of a method.
const Serializable = (Base) => class Serializable extends Base {
toJSON() { return { ...this }; }
};
class Order extends Serializable(BaseRecord) {}- Line 1 makes
Serializable, an arrow function. It takes a class calledBaseand returns a new class that extendsBase. - Line 2 is the ability the new class adds: a
toJSON()method. - Line 5 calls
Serializable(BaseRecord). That call returns a class, andOrderextends it.
Here is a complete version you can run:
class BaseRecord {
constructor(id) { this.id = id; }
}
const Serializable = (Base) => class extends Base {
toJSON() { return { id: this.id }; }
};
class Order extends Serializable(BaseRecord) {}
const order = new Order("A17");
console.log(JSON.stringify(order.toJSON()));
console.log(order instanceof BaseRecord);- Lines 1 to 3 make
BaseRecord. Its constructor stores anid. - Lines 4 to 6 make the class mixin function. The class it returns adds
toJSON(). - Line 7 makes
Orderextend the class thatSerializable(BaseRecord)returns. - Line 8 makes an order. The
BaseRecordconstructor still runs, so the order getsid"A17". - Line 9 prints
{"id":"A17"}, using the addedtoJSON(). - Line 10 prints
true. The order is still aBaseRecord, because the new class extends it.
You can wrap one mixin inside another, like Serializable(Auditable(BaseRecord)). The innermost call runs first. So the chain of classes is:
OrderextendsSerializable,- which extends
Auditable, - which extends
BaseRecord.
When you call a method, JavaScript looks in the nearest class first. If that method calls super.describe(), the call goes one class further down. Each class can add its own text when the answer comes back.
order.describe() starts at Order and each super.describe() goes one class further right. BaseRecord answers first, then each class adds its text on the way back.Step through the full example below. Then switch the wrapping order on line 14 and step through again.
Pick a wrapping order, guess the chain of classes, then step through and check.
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 last three lines of the example print the class names in the chain. Object.getPrototypeOf(Order.prototype) gives the prototype of the class that Order extends. Its constructor.name is that class's name. When you change the wrapping order, the chain changes, and so does the text from describe().
- Use a class mixin for a small ability that works on the object and may need
super. - Use a plain function when you only turn one value into another, such as
formatPrice(price). - Use composition when the ability needs its own settings, its own data, or should be easy to swap.
This keeps each mixin small, so it doesn't grow into a second big parent class.
Traits: mixins that refuse to overwrite
GUARDED COPYYou saw that Object.assign replaces a method with the same name and gives no warning. A trait is a mixin with a rule against that: if a name is already taken, stop with an error.
JavaScript has no trait keyword. You write the rule yourself in a small helper function, like this one:
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"; } });- Lines 1 to 3:
applyTraitsgoes through every trait, and every key in each trait.Reflect.ownKeyslists all of an object's own keys. - Line 4 is the rule.
key in targetasks "does the target already have this name?" If yes, it throws an error. - Line 5 copies the property's descriptor, so a getter stays a getter.
- Lines 9 to 12 make two traits and a
Photoclass, then add both traits toPhoto.prototype. - Line 13 prints
tag:p1. - Line 14 tries to add a second
tag. The prototype already has one, so the helper throwsTrait conflict: tag.
Photo.prototype already has tag, so applyTraits throws before it copies anything. The clash shows up as an error instead of a silent swap.A bigger trait helper could also let you rename a method, leave one out, or pick which one wins. Either way, a clash becomes a choice you make, not something that happens by accident.
The helper copies with Object.getOwnPropertyDescriptor and Object.defineProperty. That copies each property's descriptor, so getters stay getters. The helper could also read other settings in the descriptor, such as enumerable and writable (whether the value can be changed).
Composition over inheritance: build from parts
COMPAREYou may hear the advice "composition over inheritance". It doesn't mean "never use extends". It means: build objects from small parts first. Use extends only when one thing clearly is a kind of another, and that won't change.
A factory function is a plain function that makes and returns an object. Here, two factory functions each make one part. A third one, createOrder, puts the parts together.
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()));- Lines 1 to 3:
createSerializablemakes a part with atoJSON()method. That method returns a copy of the order's data. - Lines 4 to 10:
createEventHubmakes a part that keeps a list of listener functions.onadds a listener, andemitcalls every listener. - Line 12 makes the order's data: an
idand astatusof"new". - Line 13 builds the order from the data, the
toJSON()part, and an event hub calledevents. - Line 16 adds a listener that changes the order's status. Line 17 calls it with
"paid". - Line 18 prints
{"id":"A17","status":"paid"}.
No class had to extend a parent class with every ability inside it. In a test, you could pass in a different event hub and leave the rest alone.
The table below compares the four ways to share an ability. Read each row as: "this pattern adds this, works best here, and needs care here."
| Pattern | What it adds | Works best when | Take care that |
|---|---|---|---|
| Mixin object | It copies methods from a plain object into a prototype. | Classes that aren't related need the same small ability. | A copy replaces a method with the same name, with no warning. Object.assign turns getters into plain values. |
| Class mixin function | It returns a new class that extends any class you give it. | The ability needs super, or you want to wrap several abilities around one class. | The wrapping order decides which method JavaScript finds first. |
| Trait | It is a mixin that throws an error when a name is already taken. | You want to copy methods, but never replace one by accident. | JavaScript has no traits built in, so you write the rule yourself. |
| Composition | It builds an object from parts the object keeps and uses. | A part keeps its own data, or should be easy to swap. | There is no super. You call each part yourself. |
Now try choosing. For each situation, pick the pattern that fits best.
- Users and Orders both need a
toJSON()method, but neither is a kind of the other. AdminUserreally is a special kind ofUserand should reuse theUserconstructor.- An Order needs logging, checking, and a payment service that a test can swap out.
- Cards, dialogs, and images all need a small
enableDrag()ability. - A custom list should work like an Array and pass
Array.isArraychecks. - A dashboard keeps a notifier, a store, and a renderer, and each one can be replaced.
Pick the pattern that fits each situation. Each answer explains why.
An event mixin: on, off, emit
LIVE PLAYGROUNDAn event is a named moment, such as "done" when a download finishes. A listener is a function that should run when that event happens. Many different classes need events, so an event mixin is a useful ability to share. It has three methods:
on(name, listener)saves a listener for an event name. It returns a function that removes the listener again.off(name, listener)removes a saved listener.emit(name, payload)calls every listener saved for that name, and passes them thepayloadvalue.
The methods are copied into the prototype, so every download shares them. But the listeners are saved in this._events, on each object itself. So each download keeps its own list of listeners.
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 saved yet. Click a Subscribe button, then click Emit done.
on, off, and emit methods shown in the code. Reset makes a new download with no listeners.The buttons call the real on, off, and emit methods from the code. Try this:
- Subscribe both listeners.
- Click Emit done. Both listeners run.
- Unsubscribe one listener.
- Emit again. Only the other listener runs.
This is enough to learn mixins. Later, Observer & pub/sub explains how to plan events in a bigger program, and Build an event emitter turns this small mixin into a complete tool.
Where you will use this
Mixins help when very different classes need the same small skill. Common examples:
- Turning objects into plain data with
toJSON(). - Checking that values are valid.
- Sending events, or writing log messages.
- Letting cards, dialogs, and images be dragged.
- Checking whether a user is allowed to do something.
Pick the smallest tool that does the job:
- Use a mixin object for two or three methods that keep no data of their own and can be copied as they are.
- Use a class mixin function when the ability needs
super, or when you want to wrap several abilities around one class. - Use a trait helper when replacing a method by accident would cause bugs, so a clash should throw an error.
- Use composition when the ability keeps its own data or should be easy to swap.
To review the background, see Delegation vs classical inheritance and Objects & references.
Common misconceptions
"Mixins are just inheritance with a different name."
A class mixin function does make new classes with extends. But the goal is different. A mixin adds one small ability to classes that aren't related. It doesn't put every class under one parent.
"Object.assign is always a safe way to add a mixin."
It is quick, not safe. It replaces methods with the same name and gives no warning. It also turns getters into plain values.
"Traits are built into JavaScript."
JavaScript has no trait keyword. You write the trait rules yourself, in a helper function that copies descriptors.
"Composition means no classes."
Composition means building from parts. A part can be a plain object, a function, an object made from a class, or a class made by a mixin.
"An event mixin is a full event system."
The small mixin here shows shared methods that keep data on each object. Real event systems add more: listeners that run only once, error handling, listeners that wait for slow work, a fixed calling order, and cleanup rules.
Practice: mixins and composition
5 EXERCISESGuess the output of each program before you run it. Use the hints if you get stuck.
A ticket class gets an archive() method from a mixin object. Run the code and type the exact line it prints.
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 ticket finds it there, and this.id is T1, so it prints T1:archived.
Two class mixins are wrapped around Base. Without running it first, guess the printed text.
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());B's name calls A's, and A's calls Base's. Base returns Base. Then A adds ->A, and B adds ->B.
This helper catches the clash instead of replacing the method. What message does it print?
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 that Person.prototype already has name. It throws Conflict: name before anything is replaced, and the catch prints the message.
Write a class mixin function that adds a label() method. What label does it print?
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 new class with a label() method. Product extends that class, so a product can call label().
Finish the event mixin, add it to Timer, and check what it logs.
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);on saves a listener under tick. After the copy into Timer.prototype, timer.emit("tick", 1) calls that listener, and it logs tick 1.
Quiz: check your understanding
7 QUESTIONSFor each code question, ask: what was copied, and in what order? Did a name clash? Was a getter read? For class mixins, write down the chain of classes first. Each answer explains why it's right or wrong.
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 code 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 one is composition?
Choose an answer to see the explanation.
Question 7 of 7What does this event mixin code 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
- A mixin is one small ability that you add to classes that aren't related.
- A mixin object is a plain object of methods.
Object.assigncopies them into a prototype. Object.assigncopies only own, listed properties. It replaces methods with the same name with no warning, and it turns getters into plain values.- A class mixin function takes a class and returns a new class that extends it. Its methods can use
super, and you can wrap several around one class. - A trait is a mixin that throws an error when a name is already taken. You write that rule yourself.
- Composition builds an object from small parts. It is often simpler than a long chain of parent classes.
- An event mixin shares
on,off, andemitwith any class.
Remember the one-liner.
A mixin adds one small ability to a class or object. Composition builds an object from small parts instead of parent classes.
Up next: OOP principles in JavaScript.