en.javascript.info/1-js/07-object-oriented-programming/13-mixins/article.md
Ilya Kantor 97c8f22bbb up
2017-03-21 17:14:05 +03:00

199 lines
5.7 KiB
Markdown

# Mixins
In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]`.
But sometimes we need such kind of thing. For instance, we have a code that implements events exchange or templating, and we'd like to be able to add these capabilities to any class easily.
What can help here is *mixins*.
As [defined in Wikipedia](https://en.wikipedia.org/wiki/Mixin), a *mixin* is a class that contains methods for use by other classes without having to be the parent class of those other classes.
In other words, a *mixin* is a class that implements a certain behavior. But we do not use it alone, we use it to add the behavior to other classes.
## A mixin example
The simplest way to make a mixin in JavaScript -- is to make an object with useful methods, that we can just copy into the prototype.
For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`:
```js run
*!*
// mixin
*/!*
let sayHiMixin = {
sayHi() {
alert("Hello " + this.name);
},
sayBye() {
alert("Bye " + this.name);
}
};
*!*
// usage:
*/!*
class User {
constructor(name) {
this.name = name;
}
}
// copy the methods
Object.assign(User.prototype, sayHiMixin);
// now User can say hi
new User("Dude").sayHi(); // Hi Dude!
```
There's no inheritance, there's a simple method copying. So `User` may extend some other class and also include the mixin to "mix-in" the additional methods.
Mixins also can also make use of inheritance.
For instance, here `sayHiMixin` inherits from `sayMixin`:
```js run
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
let sayHiMixin = {
// can use any way of prototype setting here
__proto__: sayMixin,
sayHi() {
*!*
// call parent method
*/!*
super.say("Hello " + this.name);
},
sayBye() {
super.say("Bye " + this.name);
}
};
class User {
constructor(name) {
this.name = name;
}
}
// copy the methods
Object.assign(User.prototype, sayHiMixin);
// now User can say hi
new User("Dude").sayHi(); // Hi Dude!
```
Please note that the call to the parent method `super.say()` from `sayHiMixin` looks for the method in the prototype of that mixin, not the class.
![](mixin-inheritance.png)
That's because methods from `sayHiMixin` have `[[HomeObject]]` set to it. So `super` actually means `sayHiMixin.__proto__`, not `User.__proto__`.
## EventMixin
Now a mixin for the real life.
The important feature of many objects is working with events.
That is: an object should have a method to "generate an event" when something important happens to him, and other objects should be able to "subscribe" to receive such notifications.
An event must have a name and, if necessary, the attached data.
For instance, an object `user` can generate an event `"login"` when the visitor logs in. And an object `calendar` may want to receive such notifications and load the information about that visitor.
Or, the object `menu` can generate the event `"select"` when a menu item is selected, and other objects may want to get that information and react on that event.
Events is a way to "share information" with anyone who wants it.
The `eventMixin` to implement the corresponding methods:
```js run
let eventMixin = {
/**
* Subscribe to event, usage:
* menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* Cancel the subscription, usage:
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers && this._eventHandlers[eventName];
if (!handlers) return;
for(let i = 0; i < handlers.length; i++) {
if (handlers[i] == handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Generate the event and attach the data to it
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
return; // no handlers for that event name
}
// call the handlers
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
```
There are 3 methods here:
1. `.on(eventName, handler)` -- assigns the function `handler` to run when the event with that name happens. The handlers are stored in `_eventHandlers` property.
2. `.off(eventName, handler)` -- removes the function from the handlers list.
3. `.trigger(eventName, ...args)` -- generates the event: all assigned handlers are called and `args` are passed as arguments to them.
Usage:
```js run
// Make a class
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// Add the mixin
Object.assign(Menu.prototype, eventMixin);
let menu = new Menu();
// call the handler on selection:
*!*
menu.on("select", value => alert("Value selected: " + value));
*/!*
// triggers the event => shows Value selected: 123
menu.choose("123"); // value selected
```
Now if we have the code interested to react on user selection, we can bind it with `menu.on(...)`.
And the `eventMixin` can add such behavior to as many classes as we'd like, without interfering with the inheritance chain.
## Summary
*Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes.
In JavaScript that can be implemented as copying them into the prototype.
We can use mixins as a way to augment a class by multiple behaviors like event-handling that we overlooked 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 minimalize such possibility.