在 JavaScript 中,我们只能从一个对象继承。一个对象只能有一个 [[Prototype]]
。一个类也可能只扩展另一个类。
但有时这会让人感觉受到限制。例如,我们有一个类 StreetSweeper
和一个类 Bicycle
,并希望将它们混合:一个 StreetSweepingBicycle
。
或者我们有一个类User
和一个实现事件生成的类EventEmitter
,并且我们希望将EventEmitter
的功能添加到User
中,以便我们的用户可以发出事件。
有一个概念可以帮助我们,称为“mixins”。
正如维基百科中所定义的,mixin是一个包含方法的类,其他类可以使用这些方法,而无需从该类继承。
换句话说,mixin提供实现特定行为的方法,但我们不单独使用它,我们使用它将行为添加到其他类。
mixin 示例
在 JavaScript 中实现 mixin 的最简单方法是创建一个包含有用方法的对象,以便我们可以轻松地将它们合并到任何类的原型中。
例如,这里 mixin sayHiMixin
用于为User
添加一些“语音”
// 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(); // Hello Dude!
没有继承,而是一种简单的复制方法。因此,User
可以从另一个类继承,也可以包含 mixin 来“混合”其他方法,如下所示
class User extends Person {
// ...
}
Object.assign(User.prototype, sayHiMixin);
Mixins 可以利用内部的继承。
例如,这里sayHiMixin
继承自sayMixin
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
let sayHiMixin = {
__proto__: sayMixin, // (or we could use Object.setPrototypeOf to set the prototype here)
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(); // Hello Dude!
请注意,从sayHiMixin
(在标记为(*)
的行中)调用父方法super.say()
会在该 mixin 的原型中查找方法,而不是类。
这是图表(参见右侧)
这是因为方法sayHi
和sayBye
最初是在sayHiMixin
中创建的。因此,即使它们被复制了,它们的[[HomeObject]]
内部属性也会引用sayHiMixin
,如上图所示。
由于super
在[[HomeObject]].[[Prototype]]
中查找父方法,这意味着它搜索sayHiMixin.[[Prototype]]
。
EventMixin
现在让我们做一个真实的 mixin。
许多浏览器对象(例如)的一个重要特性是它们可以生成事件。事件是一种向任何想要它的人“广播信息”的好方法。因此,让我们创建一个 mixin,它允许我们轻松地将与事件相关的函数添加到任何类/对象中。
- 当重要的事情发生时,mixin 将提供一个方法
.trigger(name, [...data])
来“生成事件”。name
参数是事件的名称,后面可以跟上包含事件数据的其他参数。 - 还有方法
.on(name, handler)
,它将handler
函数添加为给定名称的事件的侦听器。当给定的name
触发事件时,它将被调用,并从.trigger
调用中获取参数。 - …还有方法
.off(name, handler)
,它删除handler
侦听器。
添加 mixin 后,当访问者登录时,user
对象将能够生成事件 "login"
。而另一个对象,比如 calendar
,可能需要监听此类事件,以便为已登录人员加载日历。
或者,当选择菜单项时,menu
可以生成事件 "select"
,而其他对象可以分配处理程序来对该事件做出反应。依此类推。
以下是代码
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?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Generate an event with the given name and data
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // no handlers for that event name
}
// call the handlers
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
.on(eventName, handler)
– 分配函数handler
,以便在发生具有该名称的事件时运行。从技术上讲,有一个_eventHandlers
属性,用于存储每个事件名称的处理程序数组,并且它只是将其添加到列表中。.off(eventName, handler)
– 从处理程序列表中删除函数。.trigger(eventName, ...args)
– 生成事件:调用_eventHandlers[eventName]
中的所有处理程序,并附带参数列表...args
。
用法
// Make a class
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// Add the mixin with event-related methods
Object.assign(Menu.prototype, eventMixin);
let menu = new Menu();
// add a handler, to be called on selection:
menu.on("select", value => alert(`Value selected: ${value}`));
// triggers the event => the handler above runs and shows:
// Value selected: 123
menu.choose("123");
现在,如果我们希望任何代码对菜单选择做出反应,我们可以使用 menu.on(...)
监听它。
并且 eventMixin
mixin 可以轻松地将这种行为添加到任意数量的类中,而不会干扰继承链。
总结
Mixin – 是一个通用的面向对象编程术语:一个包含其他类方法的类。
一些其他语言允许多重继承。JavaScript 不支持多重继承,但可以通过将方法复制到原型中来实现 mixin。
我们可以使用 mixin 作为通过添加多个行为(如我们上面看到的事件处理)来扩充类的途径。
如果 mixin 意外覆盖现有类方法,它们可能会成为冲突点。因此,通常应该仔细考虑 mixin 的命名方法,以最大程度地降低发生这种情况的可能性。
评论
<code>
标记,对于多行 – 将它们包装在<pre>
标记中,对于 10 行以上 – 使用沙箱(plnkr、jsbin、codepen…)