一、在 JavaScript 中,我们只能继承单个对象。每个对象只能有一个[[Prototype]]。并且每个类只可以扩展另外一个类。
二、但是有些时候这种设定(译注:单继承)会让人感到受限制。
1、例如,我有一个StreetSweeper类和一个Bicycle类,现在想要一个它们的 mixin:StreetSweepingBicycle类。
2、或者,我们有一个User类和一个EventEmitter类来实现事件生成(event generation),并且我们想将EventEmitter的功能添加到User中,以便我们的用户可以触发事件(emit event)。
三、有一个概念可以帮助我们,叫做 “mixins”。
1、根据维基百科的定义,mixin是一个包含可被其他类使用而无需继承的方法的类。
2、换句话说,mixin提供了实现特定行为的方法,但是我们不单独使用它,而是使用它来将这些行为添加到其他类中。
四、Mixin— 是一个通用的面向对象编程术语:一个包含其他类的方法的类。
1、一些其它编程语言允许多重继承。JavaScript 不支持多重继承,但是可以通过将方法拷贝到原型中来实现 mixin。
2、我们可以使用 mixin 作为一种通过添加多种行为(例如上文中所提到的事件处理)来扩充类的方法。
3、如果 Mixins 意外覆盖了现有类的方法,那么它们可能会成为一个冲突点。因此,通常应该仔细考虑 mixin 的命名方法,以最大程度地降低发生这种冲突的可能性。

一个 Mixin 实例

一、在 JavaScript 中构造一个 mixin 最简单的方式就是构造一个拥有实用方法的对象,以便我们可以轻松地将这些实用的方法合并到任何类的原型中。
【示例1】这个名为sayHiMixin的 mixin 用于给User添加一些“语言功能”:

  1. // mixin
  2. let sayHiMixin = {
  3. sayHi() {
  4. alert(`Hello ${this.name}`);
  5. },
  6. sayBye() {
  7. alert(`Bye ${this.name}`);
  8. }
  9. };
  10. // 用法:
  11. class User {
  12. constructor(name) {
  13. this.name = name;
  14. }
  15. }
  16. // 拷贝方法
  17. Object.assign(User.prototype, sayHiMixin);
  18. // 现在 User 可以打招呼了
  19. new User("Dude").sayHi(); // Hello Dude!

1、这里没有继承,只有一个简单的方法拷贝。所以User可以从另一个类继承,还可以包括 mixin 来 “mix-in“ 其它方法,就像这样:

  1. class User extends Person {
  2. // ...
  3. }
  4. Object.assign(User.prototype, sayHiMixin);

二、Mixin 可以在自己内部使用继承。
【示例1】这里的sayHiMixin继承自sayMixin:

  1. let sayMixin = {
  2. say(phrase) {
  3. alert(phrase);
  4. }
  5. };
  6. let sayHiMixin = {
  7. __proto__: sayMixin, // (或者,我们可以在这儿使用 Object.create 来设置原型)
  8. sayHi() {
  9. // 调用父类方法
  10. super.say(`Hello ${this.name}`); // (*)
  11. },
  12. sayBye() {
  13. super.say(`Bye ${this.name}`); // (*)
  14. }
  15. };
  16. class User {
  17. constructor(name) {
  18. this.name = name;
  19. }
  20. }
  21. // 拷贝方法
  22. Object.assign(User.prototype, sayHiMixin);
  23. // 现在 User 可以打招呼了
  24. new User("Dude").sayHi(); // Hello Dude!

1、请注意,在sayHiMixin内部对父类方法super.say()的调用(在标有(*)的行)会在 mixin 的原型中查找方法,而不是在 class 中查找。
2、这是示意图(请参见图中右侧部分):image.png

3、这是因为方法sayHi和sayBye最初是在sayHiMixin中创建的。因此,即使复制了它们,但是它们的[[HomeObject]]内部属性仍引用的是sayHiMixin,如上图所示。
4、当super在[[HomeObject]].[[Prototype]]中寻找父方法时,意味着它搜索的是sayHiMixin.[[Prototype]],而不是User.[[Prototype]]。

EventMixin

一、现在让我们为实际运用构造一个 mixin。
1、例如,许多浏览器对象的一个重要功能是它们可以生成事件。事件是向任何有需要的人“广播信息”的好方法。2、因此,让我们构造一个 mixin,使我们能够轻松地将与事件相关的函数添加到任意 class/object 中。

  • Mixin 将提供.trigger(name, […data])方法,以在发生重要的事情时“生成一个事件”。name参数(arguments)是事件的名称,[…data]是可选的带有事件数据的其他参数(arguments)。
  • 此外还有.on(name, handler)方法,它为具有给定名称的事件添加了handler函数作为监听器(listener)。当具有给定name的事件触发时将调用该方法,并从.trigger调用中获取参数(arguments)。
  • ……还有.off(name, handler)方法,它会删除handler监听器(listener)。

3、添加完 mixin 后,对象user将能够在访客登录时生成事件”login”。另一个对象,例如calendar可能希望监听此类事件以便为登录的人加载日历。
4、或者,当一个菜单项被选中时,menu可以生成”select”事件,其他对象可以分配处理程序以对该事件作出反应。诸如此类。
二、下面是代码:

let eventMixin = {
  /**
   * 订阅事件,用法:
   *  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);
  },

  /**
   * 取消订阅,用法:
   *  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);
      }
    }
  },

  /**
   * 生成具有给定名称和数据的事件
   *  this.trigger('select', data1, data2);
   */
  trigger(eventName, ...args) {
    if (!this._eventHandlers?.[eventName]) {
      return; // 该事件名称没有对应的事件处理程序(handler)
    }

    // 调用事件处理程序(handler)
    this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
  }
};

1、.on(eventName, handler)— 指定函数handler以在具有对应名称的事件发生时运行。从技术上讲,这儿有一个用于存储每个事件名称对应的处理程序(handler)的_eventHandlers属性,在这儿该属性就会将刚刚指定的这个handler添加到列表中。
2、.off(eventName, handler)— 从处理程序列表中删除指定的函数。
3、.trigger(eventName, …args)— 生成事件:所有_eventHandlers[eventName]中的事件处理程序(handler)都被调用,并且…args会被作为参数传递给它们。
三、用法:

// 创建一个 class
class Menu {
  choose(value) {
    this.trigger("select", value);
  }
}
// 添加带有事件相关方法的 mixin
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// 添加一个事件处理程序(handler),在被选择时被调用:
menu.on("select", value => alert(`Value selected: ${value}`));

// 触发事件 => 运行上述的事件处理程序(handler)并显示:
// 被选中的值:123
menu.choose("123");

四、现在,如果我们希望任何代码对菜单选择作出反应,我们可以使用menu.on(…)进行监听。
五、使用eventMixin可以轻松地将此类行为添加到我们想要的多个类中,并且不会影响继承链。