JAVASCRIPT / PROTOTYPES, CLASSES, AND INHERITANCE
Mixins for sharing behaviour across classes
Share behaviour across unrelated classes by copying method bags onto prototypes or by splicing subclass-factory mixins into the prototype chain.
What you will learn
- Apply a bag of methods to a class with Object.assign(Class.prototype, mixin)
- Keep getters intact using Object.getOwnPropertyDescriptors with defineProperties
- Write Base => class extends Base mixins when a method needs super
- Predict which mixin wins when two of them define the same method name
Understanding Mixins for sharing behaviour across classes
A class in JavaScript has exactly one prototype link, so extends can name a single parent and nothing more. A mixin works around that by packaging behaviour with no class of its own: either a plain object full of methods, or a function that manufactures a class on demand. The mental model is that the prototype chain is a fixed sequence of objects, and a mixin is either paste applied to one object in that sequence or a brand new object spliced into it.
The copying style writes onto Class.prototype, which is the very same object where methods declared in the class body live. That means a mixin key equal to a method name silently replaces the class's own version, and the last source passed to Object.assign wins. Copying is also a snapshot taken through get and set: a getter in the mixin is invoked once during the copy, against the mixin object itself, and only its return value lands on the target as an ordinary data property.
The subclass-factory style, Base => class extends Base, avoids all of that because it produces a real link in the chain. super works from inside the mixin, an overriding method in the final subclass shadows it through normal lookup, and instances still pass instanceof against the original base. The cost is a fresh anonymous class per application and a longer chain, and because the class is anonymous there is nothing to test the mixin itself with unless you keep a reference to what the factory returned. Reach for copying when the behaviour is a small stateless method bag, and for a factory when it needs super, constructor work, or fields.
const canFly = {
fly() {
this.altitude = 100;
return `${this.name} climbs to ${this.altitude}m`;
}
};
const canSwim = {
swim() {
return `${this.name} dives`;
},
describe() {
return `${this.name} is a swimmer`;
}
};
class Bird {
constructor(name) {
this.name = name;
this.altitude = 0;
}
describe() {
return `${this.name} is a bird`;
}
}
Object.assign(Bird.prototype, canSwim, canFly);
const duck = new Bird('Duck');
console.log(duck.fly());
console.log(duck.swim());
console.log(duck.describe());
console.log(Object.hasOwn(duck, 'fly'), Object.hasOwn(Bird.prototype, 'fly'));A mixin is behaviour defined independently of any class, applied either by copying it onto a prototype or by generating a class to insert into the prototype chain.
Worked examples
Subclass factory so super still works
A mixin written as a function returning a class becomes a real link in the prototype chain, so super and instanceof keep working.
const Timestamped = (Base) => class extends Base {
constructor(...args) {
super(...args);
this.createdAt = 0;
}
describe() {
return `${super.describe()} (t=${this.createdAt})`;
}
};
class Note {
constructor(text) {
this.text = text;
}
describe() {
return `Note: ${this.text}`;
}
}
class StampedNote extends Timestamped(Note) {
describe() {
return `[${super.describe()}]`;
}
}
const n = new StampedNote('buy milk');
console.log(n.describe());
console.log(Object.getPrototypeOf(Object.getPrototypeOf(StampedNote.prototype)) === Note.prototype);
console.log(n instanceof Note);Example explained
Line 1Timestamped(Note) evaluates to an anonymous class whose parent is Note, so calling it inside extends is what inserts the mixin.
Line 2super.describe() inside the mixin resolves to Note.prototype.describe because the generated class genuinely extends Base.
Line 3StampedNote's own describe wraps the mixin's result, showing three methods cooperating through one chain of super calls.
Line 4The two getPrototypeOf steps land on Note.prototype, which is also why instanceof Note is still true.
Object.assign flattens accessors
Copying with Object.assign turns a mixin getter into a fixed value, while copying descriptors keeps it live.
const Sized = {
get size() {
return (this.items || []).length;
},
set size(n) {
this.items.length = n;
}
};
class Bag {
constructor(items) {
this.items = items;
}
}
class Sack {
constructor(items) {
this.items = items;
}
}
Object.assign(Bag.prototype, Sized);
Object.defineProperties(Sack.prototype, Object.getOwnPropertyDescriptors(Sized));
const bag = new Bag(['a', 'b']);
const sack = new Sack(['a', 'b']);
console.log(typeof Object.getOwnPropertyDescriptor(Bag.prototype, 'size').get);
console.log(typeof Object.getOwnPropertyDescriptor(Sack.prototype, 'size').get);
console.log(bag.size, sack.size);
sack.size = 1;
console.log(JSON.stringify(sack.items));Example explained
Line 1Object.assign reads Sized.size once with this pointing at Sized, so the getter returns 0 and that number is what gets copied.
Line 2Bag.prototype therefore has a data property with no get function, which is why bag.size stays 0 even though the bag holds two items.
Line 3Object.getOwnPropertyDescriptors hands defineProperties the accessor pair unchanged, so Sack.prototype keeps a real getter and reports 2.
Line 4The setter survives the descriptor copy too, so sack.size = 1 truncates the underlying array.
Composition order decides who wins
Nesting subclass factories in a different order changes which mixin method runs first.
const Loud = (Base) => class extends Base {
speak() {
return super.speak().toUpperCase();
}
};
const Polite = (Base) => class extends Base {
speak() {
return `${super.speak()}, please`;
}
};
class Speaker {
speak() {
return 'move';
}
}
class One extends Loud(Polite(Speaker)) {}
class Two extends Polite(Loud(Speaker)) {}
console.log(new One().speak());
console.log(new Two().speak());Example explained
Line 1In Loud(Polite(Speaker)) the inner call runs first, so Polite's class sits lower and Loud's class ends up as One's direct parent.
Line 2One's lookup finds Loud.speak first: it builds 'move, please' through super and then uppercases the whole string.
Line 3Two reverses the nesting, so Polite.speak runs first and appends ', please' after Loud has already uppercased 'move'.
Line 4Nothing is overwritten in either case; the order only changes where each method sits relative to the others.
Collision check before copying
Inspecting the target prototype first turns a silent method replacement into a loud error.
function mixInto(target, mixin) {
for (const key of Reflect.ownKeys(mixin)) {
if (key in target.prototype) {
throw new Error(`${String(key)} already exists on ${target.name}.prototype`);
}
}
Object.defineProperties(target.prototype, Object.getOwnPropertyDescriptors(mixin));
return target;
}
const Countable = {
count() {
return this.items.length;
}
};
const Reportable = {
count() {
return -1;
}
};
class Cart {
constructor(items) {
this.items = items;
}
}
mixInto(Cart, Countable);
console.log(new Cart(['pen', 'ink']).count());
try {
mixInto(Cart, Reportable);
} catch (err) {
console.log(err.message);
}Example explained
Line 1Reflect.ownKeys covers string and symbol keys, including the non-enumerable ones Object.keys would miss.
Line 2The in operator walks the whole chain, so the guard also catches clashes with inherited methods, not just own ones.
Line 3The first call succeeds and count() reads live state through this.items.
Line 4The second call throws instead of quietly replacing Countable.count, which is exactly the failure Object.assign hides.
Important notes
Methods copied onto a prototype are enumerable by default, unlike class methods, so they show up in for...in loops over instances; defineProperties with descriptors from a class prototype avoids that, a plain object literal does not.
A subclass factory returns an anonymous class, so there is no name to test with instanceof; store the returned class in a variable, or tag instances with a symbol property, if you need to detect that the mixin was applied.
Common mistakes
Trying to mix in another class with Object.assign(Sub.prototype, Other.prototype): methods declared in a class body are non-enumerable, so nothing is copied and the first call fails with 'is not a function'.
Copying a mixin over a name the class already defines, such as toString or describe: there is no error, the class's own version becomes unreachable, and the bug surfaces far from the Object.assign line.
Using super inside an object-literal mixin method: its home object is the literal, so super resolves against Object.prototype no matter which class you copy it onto, and you get a TypeError instead of the parent's method.
Try it yourself
Change, predict, then run
Build a Playlist class holding a tracks array, then write a Shuffleable mixin with a shuffle() method and a Countable mixin with a count getter, and apply both twice: once with Object.assign and once with Object.defineProperties plus Object.getOwnPropertyDescriptors. Push a track afterwards and report which version's count still updates and why.
Open the JavaScript workspaceCheck your understanding
Given class C extends A(B(Base)) {} where C declares no render of its own and both subclass-factory mixins define render() calling super.render(), whose render body executes first when you call new C().render()?
- B's, because B(Base) is the call evaluated first
- Base's, because super calls resolve from the bottom of the chain upward
- A's, because A's generated class ends up as C's direct parent and B's sits beyond it
- Neither, because a mixin method only runs if C forwards to it explicitly
Show answer
Lookup starts at C.prototype and stops at the first render found on the chain, which belongs to the class A produced, since A wrapped the class B produced. B is tempting because B(Base) really is evaluated first, but evaluation builds the chain from the base upward, so the earliest applied mixin ends up furthest from C and only runs when A's render delegates through super.