2022 年 8 月 27 日

Mixins

在 JavaScript 中,我們只能繼承自單一物件。物件只能有一個 [[Prototype]]。而且一個類別只能延伸自另一個類別。

但有時這會讓人覺得受到限制。例如,我們有一個類別 StreetSweeper 和一個類別 Bicycle,並希望將它們混合:一個 StreetSweepingBicycle

或者我們有一個類別 User 和一個類別 EventEmitter,它實作了事件產生,我們希望將 EventEmitter 的功能新增到 User,以便我們的使用者可以發出事件。

有一個概念可以幫助我們,稱為「mixins」。

根據維基百科的定義,混入是一種包含方法的類別,其他類別可以使用這些方法,而無需繼承自該類別。

換句話說,混入提供了實作特定行為的方法,但我們不會單獨使用它,而是使用它將行為新增到其他類別。

混入範例

在 JavaScript 中實作混入的最簡單方法是建立一個包含有用方法的物件,這樣我們可以輕鬆地將它們合併到任何類別的原型中。

例如,這裡的混入 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 可以繼承自另一個類別,也可以包含混入以「混入」額外的方法,如下所示

class User extends Person {
  // ...
}

Object.assign(User.prototype, sayHiMixin);

混入可以在它們自己內部使用繼承。

例如,這裡的 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()(在標記為 (*) 的行中)會在該混入的原型中尋找方法,而不是類別。

以下是圖表(請參閱右側)

這是因為方法 sayHisayBye 最初是在 sayHiMixin 中建立的。因此,即使它們被複製,它們的 [[HomeObject]] 內部屬性也會參照 sayHiMixin,如上圖所示。

由於 super 會在 [[HomeObject]].[[Prototype]] 中尋找父方法,這表示它會搜尋 sayHiMixin.[[Prototype]]

EventMixin

現在讓我們建立一個實際的混入。

許多瀏覽器物件(例如)的一個重要功能是它們可以產生事件。事件是一種很好的方式,可以向任何想要它的人「廣播資訊」。因此,讓我們建立一個混入,它允許我們輕鬆地將與事件相關的功能新增到任何類別/物件。

  • 混入將提供一個方法 .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 問題 或提交 pull 要求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行,請將它們包在 <pre> 標籤中,若要插入 10 行以上,請使用沙盒(plnkrjsbincodepen…)