JS Tip of the Day: Mixins

Mixins
Level: Intermediate

Mixins (or mix-ins) are a way to share functionality between multiple objects or object types. Instead of being a part of an object’s static inheritance chain (as you see with normal OOP usage), they’re “mixed in” dynamically.

While JavaScript doesn’t have native support for mixins, there are a couple of different ways to implement them yourself manually. The following sections cover a few different approaches.

Direct Copy

The first approach is the simplest. All it requires is that you have an object of values that you want to share - normally a collection of methods - and then copy those values over into the object you wish to also have them. This copy is often performed with object spreading (...) or using Object.assign().

// Mixin
let shared = {
    log () {
        console.log('Logged:', this);
    }
};

// for new objects
let obj = {
    value: 1,
    ...shared // mix it in
};
obj.log(); // Logged: {value: 1, log: ƒ}

// for existing objects
Object.assign(document, shared); // mix it in
document.log(); // Logged: HTMLDocument {}

For classes, you’ll most likely want to copy the mixin into the class’s prototype so the copy is only needed once and will be available for all instances of the class.

// Mixin
let shared = {
    log () {
        console.log('Logged:', this);
    }
};

class Data {
    constructor (value) {
        this.value = value;
    }
}
Object.assign(Data.prototype, shared); // mix it in

let data = new Data(1);
data.log(); // Logged: Data {value: 1}

The problem with this approach is that it only works well with shared methods and primitive data values. If you have data you want to share that’s a more complex data type such as an object or an array, when copied, each mixin target will share the same reference to that data.

// Mixin
let dispatcher = {
    addListener (listener) {
        this.listeners.push(listener);
    },
    listeners: [] // object data!
};
class Ear {
    constructor (head) {
        this.addListener(head);
    }
}
Object.assign(Ear.prototype, dispatcher); // mix it in

let leftEar = new Ear('head');
console.log(leftEar.listeners.length); // 1

let rightEar = new Ear('head');
console.log(rightEar.listeners.length); // 2!
console.log(leftEar.listeners === rightEar.listeners); // true

What we really want is for each instance to have its own listeners array, but because there is only one, and that same array is shared between all instances, whenever one instance changes that array, it changes for all other instances as well. That can be solved with the next approach.

Copy With Init

This approach is similar to using direct copy. In fact it will continue to do that, only it also introduces a custom copy function represented as initialize() or init(). This function is defined by the mixin to handle the necessary copy of shared mixin properties along with any other necessary set up that may be required including, but not limited to, making sure each target gets its own copy of data variables.

// Mixin
let dispatcher = {
    init (target) {
        target.listeners = []; // new for each init call
        Object.assign(target, this.shared);
    },
    shared: {
        addListener (listener) {
            this.listeners.push(listener);
        }
    }
};
class Ear {
    constructor (head) {
        dispatcher.init(this); // mix it in
        this.addListener(head);
    }
}

let leftEar = new Ear('head');
console.log(leftEar.listeners.length); // 1

let rightEar = new Ear('head');
console.log(leftEar.listeners.length); // 1
console.log(leftEar.listeners === rightEar.listeners); // false

Notice that because the init() method performs per-instance initialization, it is necessary to called for each instance in the constructor rather than once on the class’s prototype. This ensures each instance gets its own copy of the data (listeners) needed for the mixin functionality. Methods ( addListener) are still copied in with a direct copy which is OK because, not being data members, each instance doesn’t need its own copy of the same function. They can be shared just like methods are shared from prototypes. You could also copy the methods into the prototype, but you’d want to do that in a separate step (after class creation) so that it would not be duplicated each init call.

Class-based Mixins

Mixins can also be performed at the class level, dynamically mixing in new classes into an inheritance hierarchy. The advantage with this approach is that you get all the power and flexibility of classes with the reusability of mixins.

Class-based mixins use a function to have one class extend another. This extending happens dynamically when the function is called using a new class for each call. Because a new class is created each time the mixin is applied, it can be re-used multiple times on multiple classes with different inheritance hierarchies.

// Mixin
function counter (Base) {
    return class extends Base {
        constructor (...args) {
            super(...args);
            this.count = 0;
        }
        increment () {
            this.count++;
        }
    }
}
class Knob {}
let CountingKnob = counter(Knob); // mix it in

let knob = new CountingKnob();
console.log(knob.count); // 0
knob.increment();
console.log(knob.count); // 1

class Door {}
let CountingDoor = counter(Door); // mix it in

let door = new CountingDoor();
console.log(door.count); // 0
door.increment();
console.log(door.count); // 1

Here the counter mixin is getting applied to two different classes, Knob, and Door. Despite the fact that each mixin class is functionally the same, because new versions of those class are created each time the mixin function is called, they’re able to each have independent inheritance hierarchies, one extending Knob and the other extending Door. If the counter mixin class was defined as a single, normal class rather than being created dynamically in the function, it would only be able to extend one other class.

Given the dynamic nature of JavaScript, you can even call these mixin functions in the class extends clause.

class Turnstile extends counter(Door) { // mix it in
    spin () {
        this.increment();
    }
}

let door = new Turnstile();
console.log(door.count); // 0
door.spin();
console.log(door.count); // 1

The downside of the class-based mixin approach is that each time the mixin is used, a unique copy of the mixin class is created. This creates not only duplicate classes but duplicates for all methods of the mixin class as well.

The TypeScript documentation also has a section covering mixins, but I would not recommend their approach. While it looks like its a class-based, it ultimately boils down being a direct copy, going through the prototype of the mixin class. This means the constructor of the mixin is never called and there’s never an opportunity to set data members (unless done through calling the copied methods which their examples do).

More info:

3 Likes