2022 年 8 月 27 日

Mixins

在 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 的原型中查找方法,而不是类。

这是图表(参见右侧)

这是因为方法sayHisayBye最初是在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 的命名方法,以最大程度地降低发生这种情况的可能性。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 – 将它们包装在 <pre> 标记中,对于 10 行以上 – 使用沙箱(plnkrjsbincodepen…)