ref – https://javascript.info/mixins
a mixin provides methods that implement a certain behavior, but we do not use it alone, we use it to
add to the behavior to other classes
.
In other words, mixins are used to extend the behavior of objects by providing a set of reusable functions.
It is used to modularize functionality. For example, if you have a class with all sorts of functionalities, it will become messy and unmanageable. But if you use mixins, you can modularize the functionality and thus, making it much easier to maintain.
The simplest way to make a mixin in JavaScript is to make an object with useful methods. This acts as a prototype object. We then can create other literal objects and extend from this object. This adds additional functionalities to it. In essence, we are “mixing in” additional methods. Finally, we take this hierarchy of a prototype object, and can easily merge them into a prototype of any class.
Creating a mixin
First, we’ll create a literal object “sayMixin”. It will act as a base prototype.
Note that all objects derive from “Object”. When we create our literal object, its __proto__ will automatically be referencing Object Prototype. In the diagram, you’ll notice “extend” in Object Prototype. That’s simply a function defined in a previous example:
1 2 3 4 |
Object.prototype.extend = function (extension) { ... ... } |
So whatever functionality you add to Object prototype will be inherited by all objects you create.
On to the example…
1 2 3 4 5 6 |
// declare literal object let sayMixin = { say(phrase) { console.log(phrase); } }; |
Thne, we want to extend from sayMixin. So we create another literal object. A literal object has a __proto__ property. In order to extend this object from another, we point the __proto__ to the base object “sayMixin”.
Then, we define our functionality, and in order to call the base object, we use super. Then we simply access the functions/properties of super.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// declare another literal object, that derives from sayMixin // via __proto__ let sayHiMixin = { __proto__: sayMixin, // (or we could use Object.create to set the prototype here) // declare own functions sayHi() { // call parent method. Notice usage of "super" super.say(`Hello ${this.name}`); }, sayBye() { super.say(`Bye ${this.name}`); } }; |
The digram of the hierarchy looks like this:
Using the Mixin
Now that we have our mixin prototype object created, we want to use start using it!
So first, we create a constructor function User.
1 2 3 |
function User(newName) { this.name = newName; } |
Then we add functionality to its prototype like so:
1 2 3 |
User.prototype.logInfo = function() { console.log(this.name + ", is a cool person!") } |
“new” will be used to instantiate it.
1 |
var dude = new User("Dude"); |
So diagram wise, it looks like this:
in order to add our mixin prototype features into it, we use Object.assign to carry all owned properties and functions over to User. This is so that you don’t over-ride anything in User’s prototype object, such as logInfo.
By definition
The Object.assign() method only copies enumerable and own properties from a source object to a target object.
1 2 |
// copy the methods Object.assign(User.prototype, sayHiMixin); |
Thus, we created a base prototype object in sayMixin, extended it to sayHiMixin, and finally, copied all its traits to User’s prototype object.
Hence, bringing over a new prototype object into User’s current available prototype object, and “mixin” them via Object.assign is what this is all about.
Thus, now we have successfully “mixed in” another prototype into User’s prototype.
1 2 3 4 5 6 |
var dude = new User("Dude"); console.log(dude) console.log(dude.__proto__) dude.logInfo(); dude.sayHi(); dude.sayBye(); |
output:
User { name: ‘Dude’ }
User {
logInfo: [Function],
sayHi: [Function: sayHi],
sayBye: [Function: sayBye]
}
Dude, is a cool person!
Hello Dude
Bye Dude
Mixins can make use of inheritance inside themselves
First, we set up the skeleton code.
We create a literal object with function properties. Each function takes a string for the event name, and a function handler so that we have a reference to the function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let eventMixin = { on(eventName, funcHandler) { console.log("on called, with eventName: " + eventName) funcHandler(); }, off(eventName, funcHandler) { console.log("OFF called, with eventName: " + eventName) }, trigger(eventName, ...args) { console.log("trigger: " + eventName) } } // end of eventMixin console.log("create Menu class") |
Then we make a class called Menu. This class has a function called choose. When executed, calling “choose” will make whatever object it owns to call trigger.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Make a class class Menu { choose(value) { this.trigger("select", value); } } console.log(Menu.prototype) //Menu {} // this is where the mixin happens. We are copying on, off, trigger functionality into // a class in order to augment and enhance it. Object.assign(Menu.prototype, eventMixin); /* Menu { on: [Function: on], off: [Function: off], trigger: [Function: trigger] } */ console.log(Menu.prototype) let menu = new Menu(); menu.on("select", value => { console.log("handler called") }); |
Now, the idea is that an object should have a method to “generate an event” when something important happens to it, and other objects should be able to “listen” to such events.
full source
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
// we store an object of arrays. // the arrays contain handlers, or functions. // each array is identified by a string called eventName. let eventMixin = { // subscribe to the event. on(eventName, funcHandler) { console.log("on called, with eventName: " + eventName) // lazy load eventHandlers if (!this._eventHandlers) this._eventHandlers = {}; // given a string name, does its array of handlers exist? // if not, just create an array to store handlers if (!this._eventHandlers[eventName]) { this._eventHandlers[eventName] = []; } // if the string does exist, let's just add one more handler to it! this._eventHandlers[eventName].push(funcHandler); }, off(eventName, funcHandler) { console.log("OFF called, with eventName: " + eventName) }, trigger(eventName, ...args) { console.log("trigger: " + eventName) if(!this._eventHandlers || !this._eventHandlers[eventName]) { console.log("trigger - no handlers for this event"); return; } //Array.prototype.forEach() this._eventHandlers[eventName].forEach(handler => { console.log("for each handler") // "this" is the menu object console.log(args) // array [...], where 1st element is the length // so we get [3] // we call the handler function on "this" object // we pass in array of arguments called args. handler.apply(this, args) //handler.apply(this, args) }); } } // end of eventMixin console.log("create Menu class") // Make a class class Menu { choose(value) { this.trigger("select", value); } } console.log(Menu.prototype) //Menu {} // copy it over to Menu.prototype Object.assign(Menu.prototype, eventMixin); /* Menu { on: [Function: on], off: [Function: off], trigger: [Function: trigger] } */ console.log(Menu.prototype) let menu = new Menu(); // a menu can generate the event "select" when a menu item is selected. // ..and there may be many different objects that want to know about it. // we can call the handler on selection var objA = { sendOff() { console.log(" send email to my peeps about this menu selection") } } var objB = { openPage(){ console.log("√ open another web page") } } var objC = { sendEmail() { console.log("∑mail the user!") } } menu.on("select", objA.sendOff); menu.on("select", objB.openPage); menu.on("select", objC.sendEmail); // now the menu will NOTIFY all objects. menu.choose(3) |
Now, you see, when the menu gets selected, we can notify other objects via callbacks.
JavaScript does not support multiple inheritance, but mixins can be implemented by copying them into the prototype.
We can use mixins as a way to augment a class by multiple behaviors, like event-handling as we have seen above.
Mixins may become a point of conflict if they occasionally overwrite native class methods. So generally one should think well about the naming for a mixin, to minimize such possibility.
BluePrints for mixins
Sometimes however mixins require private state. For example the eventEmitter mixin would be more secure if it stored its event listeners in a private variable instead of on the “this” object.
To recap, we created a menu like so:
1 |
let menu = new Menu(); |
we created an object menu, with its prototype having properties from eventMixin.
When we register an object for an event say “select”:
1 |
menu.on("select", objA.sendOff); |
the on function has lazy loaded the property _eventHandlers, which is object of “string : array” index value pairs.
1 2 3 4 5 6 7 8 9 |
on(eventName, funcHandler) { // lazy load eventHandlers if (!this._eventHandlers) this._eventHandlers = {}; ... ... } |
at this point if you were to analyze the _eventHandlers property:
1 |
console.log(menu._eventHandlers) |
output:
{ select:
[ [Function: sendOff],
[Function: openPage],
[Function: sendEmail] ] }
It would show that the “select” value would match up to three handlers.
This public access of this property is bad, and this is what we’re trying to solve. However mixins have no create function to encapsulate private state. Hence we create “blueprints” of mixins to create closures. Blueprints may look like constructor functions but they are not meant to be used as constructors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function eventEmitter() { var events = Object.create(null); // private // public function this.on = function (event, listener) { // use private var if (typeof events[event] !== "undefined") events[event].push(listener); else events[event] = [listener]; }; // public function this.emit = function (event) { if (typeof events[event] !== "undefined") { var listeners = events[event]; var length = listeners.length, index = length; var args = Array.prototype.slice.call(arguments, 1); while (index) { var listener = listeners[length - (index--)]; listener.apply(this, args); } } }; } |
A blueprint is used to extend an object via concatenation after it’s created.
In the create function, you then use the allocated object in rectangle.extend (self), and use .call on it for eventEmitter.
At that step, you then append all functionalities in eventEmitter onto the allocated object from rectangle.create.call.
That way, private variable events stay in eventEmitter. And eventEmitter’s functions stay public for the allocated object form rectangle.extend.
1 2 3 4 5 6 7 8 9 10 11 12 |
var square = rectangle.extend({ create: function (side) { var self = rectangle.create.call(this, side, side); eventEmitter.call(self); // extend an object via concatenation return self; }, resize: function (newSize) { var oldSize = this.width; this.width = this.height = newSize; this.emit("resize", oldSize, newSize); } }); |
Blueprints are unique to JavaScript. They are a powerful feature. However they have their own disadvantages. The following table compares the advantages and disadvantages of mixins and blueprints:
mixins
They are used to extend prototypes of objects. Hence objects share the same blueprint functions.
No private state due to lack of an encapsulating function.
They are static prototypes and can’t be customized.
blueprints
They are used to extend newly created objects. Hence every object has its own set of blueprint functions.
They are functions, and hence they encapsulate private state.
They can be passed arguments to customize the object.