Javascript Mixin 模式

2023-02-17 10:53 更新

在 JavaScript 中,我們只能繼承單個對象。每個對象只能有一個 ?[[Prototype]]?。并且每個類只可以擴展另外一個類。

但是有些時候這種設定(譯注:單繼承)會讓人感到受限制。例如,我有一個 StreetSweeper 類和一個 Bicycle 類,現(xiàn)在想要一個它們的 mixin:StreetSweepingBicycle 類。

或者,我們有一個 User 類和一個 EventEmitter 類來實現(xiàn)事件生成(event generation),并且我們想將 EventEmitter 的功能添加到 User 中,以便我們的用戶可以觸發(fā)事件(emit event)。

有一個概念可以幫助我們,叫做 “mixins”。

根據(jù)維基百科的定義,mixin 是一個包含可被其他類使用而無需繼承的方法的類。

換句話說,mixin 提供了實現(xiàn)特定行為的方法,但是我們不單獨使用它,而是使用它來將這些行為添加到其他類中。

一個 Mixin 實例

在 JavaScript 中構(gòu)造一個 mixin 最簡單的方式就是構(gòu)造一個擁有實用方法的對象,以便我們可以輕松地將這些實用的方法合并到任何類的原型中。

例如,這個名為 sayHiMixin 的 mixin 用于給 User 添加一些“語言功能”:

// mixin
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};

// 用法:
class User {
  constructor(name) {
    this.name = name;
  }
}

// 拷貝方法
Object.assign(User.prototype, sayHiMixin);

// 現(xiàn)在 User 可以打招呼了
new User("Dude").sayHi(); // Hello Dude!

這里沒有繼承,只有一個簡單的方法拷貝。所以 User 可以從另一個類繼承,還可以包括 mixin 來 "mix-in“ 其它方法,就像這樣:

class User extends Person {
  // ...
}

Object.assign(User.prototype, sayHiMixin);

Mixin 可以在自己內(nèi)部使用繼承。

例如,這里的 sayHiMixin 繼承自 sayMixin

let sayMixin = {
  say(phrase) {
    alert(phrase);
  }
};

let sayHiMixin = {
  __proto__: sayMixin, // (或者,我們可以在這兒使用 Object.setPrototypeOf 來設置原型)

  sayHi() {
    // 調(diào)用父類方法
    super.say(`Hello ${this.name}`); // (*)
  },
  sayBye() {
    super.say(`Bye ${this.name}`); // (*)
  }
};

class User {
  constructor(name) {
    this.name = name;
  }
}

// 拷貝方法
Object.assign(User.prototype, sayHiMixin);

// 現(xiàn)在 User 可以打招呼了
new User("Dude").sayHi(); // Hello Dude!

請注意,在 sayHiMixin 內(nèi)部對父類方法 super.say() 的調(diào)用(在標有 (*) 的行)會在 mixin 的原型中查找方法,而不是在 class 中查找。

這是示意圖(請參見圖中右側(cè)部分):


這是因為方法 sayHi 和 sayBye 最初是在 sayHiMixin 中創(chuàng)建的。因此,即使復制了它們,但是它們的 [[HomeObject]] 內(nèi)部屬性仍引用的是 sayHiMixin,如上圖所示。

當 super 在 [[HomeObject]].[[Prototype]] 中尋找父方法時,意味著它搜索的是 sayHiMixin.[[Prototype]],而不是 User.[[Prototype]]。

EventMixin

現(xiàn)在讓我們?yōu)閷嶋H運用構(gòu)造一個 mixin。

例如,許多瀏覽器對象的一個重要功能是它們可以生成事件。事件是向任何有需要的人“廣播信息”的好方法。因此,讓我們構(gòu)造一個 mixin,使我們能夠輕松地將與事件相關(guān)的函數(shù)添加到任意 class/object 中。

  • Mixin 將提供 ?.trigger(name, [...data])? 方法,以在發(fā)生重要的事情時“生成一個事件”。?name? 參數(shù)(arguments)是事件的名稱,?[...data]? 是可選的帶有事件數(shù)據(jù)的其他參數(shù)(arguments)。
  • 此外還有 ?.on(name, handler)? 方法,它為具有給定名稱的事件添加了 ?handler? 函數(shù)作為監(jiān)聽器(listener)。當具有給定 ?name? 的事件觸發(fā)時將調(diào)用該方法,并從 ?.trigger? 調(diào)用中獲取參數(shù)(arguments)。
  • ……還有 ?.off(name, handler)? 方法,它會刪除 ?handler? 監(jiān)聽器(listener)。

添加完 mixin 后,對象 user 將能夠在訪客登錄時生成事件 "login"。另一個對象,例如 calendar 可能希望監(jiān)聽此類事件以便為登錄的人加載日歷。

或者,當一個菜單項被選中時,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);
      }
    }
  },

  /**
   * 生成具有給定名稱和數(shù)據(jù)的事件
   *  this.trigger('select', data1, data2);
   */
  trigger(eventName, ...args) {
    if (!this._eventHandlers?.[eventName]) {
      return; // 該事件名稱沒有對應的事件處理程序(handler)
    }

    // 調(diào)用事件處理程序(handler)
    this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
  }
};

  1. ?.on(eventName, handler)? — 指定函數(shù) ?handler? 以在具有對應名稱的事件發(fā)生時運行。從技術(shù)上講,這兒有一個用于存儲每個事件名稱對應的處理程序(handler)的 ?_eventHandlers? 屬性,在這兒該屬性就會將剛剛指定的這個 ?handler? 添加到列表中。
  2. ?.off(eventName, handler)? — 從處理程序列表中刪除指定的函數(shù)。
  3. ?.trigger(eventName, ...args?) — 生成事件:所有 ?_eventHandlers[eventName]? 中的事件處理程序(handler)都被調(diào)用,并且 ?...args? 會被作為參數(shù)傳遞給它們。

用法:

// 創(chuàng)建一個 class
class Menu {
  choose(value) {
    this.trigger("select", value);
  }
}
// 添加帶有事件相關(guān)方法的 mixin
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// 添加一個事件處理程序(handler),在被選擇時被調(diào)用:
menu.on("select", value => alert(`Value selected: ${value}`));

// 觸發(fā)事件 => 運行上述的事件處理程序(handler)并顯示:
// 被選中的值:123
menu.choose("123");

現(xiàn)在,如果我們希望任何代碼對菜單選擇作出反應,我們可以使用 menu.on(...) 進行監(jiān)聽。

使用 eventMixin 可以輕松地將此類行為添加到我們想要的多個類中,并且不會影響繼承鏈。

總結(jié)

Mixin —— 是一個通用的面向?qū)ο缶幊绦g(shù)語:一個包含其他類的方法的類。

一些其它編程語言允許多重繼承。JavaScript 不支持多重繼承,但是可以通過將方法拷貝到原型中來實現(xiàn) mixin。

我們可以使用 mixin 作為一種通過添加多種行為(例如上文中所提到的事件處理)來擴充類的方法。

如果 Mixins 意外覆蓋了現(xiàn)有類的方法,那么它們可能會成為一個沖突點。因此,通常應該仔細考慮 mixin 的命名方法,以最大程度地降低發(fā)生這種沖突的可能性。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號