在 JavaScript 中,我們只能繼承單個對象。每個對象只能有壹個 [[Prototype]]
。並且每個類只可以擴展另外壹個類。
但是有些時候這種設定(譯注:單繼承)會讓人感到受限制。例如,我有壹個 StreetSweeper
類和壹個 Bicycle
類,現在想要壹個它們的混合體:StreetSweepingBicycle
類。
或者,我們有壹個 User
類和壹個 EventEmitter
類來實現事件生成(event generation),並且我們想將 EventEmitter
的功能添加到 User
中,以便我們的用戶可以觸發事件(emit event)。
有壹個概念可以幫助我們,叫做 “mixin”。
根據維基百科的定義,mixin 是壹個類,其方法可被其他類使用,而無需繼承。
換句話說,mixin 提供了實現特定行爲的方法,但是我們不單獨使用它,而是使用它來將這些行爲添加到其他類中。
在 JavaScript 中構造壹個 mixin 最簡單的方式就是構造壹個擁有實用方法的對象,以便我們可以輕松地將這些實用的方法合並到任何類的原型中。
例如,這個名爲 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); // 現在 User 可以打招呼了 new User("Dude").sayHi(); // Hello Dude!
這裏沒有繼承,只有壹個簡單的方法拷貝。因此,我們可以讓 User
在繼承另壹個類的同時,使用 mixin 來 “mix-in”(混合)其它方法,就像這樣:
class User extends Person { // ... } Object.assign(User.prototype, sayHiMixin);
Mixin 可以在自己內部使用繼承。
例如,這裏的 sayHiMixin
繼承自 sayMixin
:
let sayMixin = { say(phrase) { alert(phrase); } }; let sayHiMixin = { __proto__: sayMixin, // (或者,我們可以在這兒使用 Object.setPrototypeOf 來設置原型) sayHi() { // 調用父類方法 super.say(`Hello ${this.name}`); // (*) }, sayBye() { super.say(`Bye ${this.name}`); // (*) } }; class User { constructor(name) { this.name = name; } } // 拷貝方法 Object.assign(User.prototype, sayHiMixin); // 現在 User 可以打招呼了 new User("Dude").sayHi(); // Hello Dude!
請注意,在 sayHiMixin
內部對父類方法 super.say()
的調用(在標有 (*)
的行)會在 mixin 的原型中查找方法,而不是在 class 中查找。
這是示意圖(請參見圖中右側部分):
這是因爲方法 sayHi
和 sayBye
最初是在 sayHiMixin
中創建的。因此,即使複制了它們,但是它們的 [[HomeObject]]
內部屬性仍引用的是 sayHiMixin
,如上圖所示。
當 super
在 [[HomeObject]].[[Prototype]]
中尋找父方法時,意味著它搜索的是 sayHiMixin.[[Prototype]]
,而不是 User.[[Prototype]]
。
現在讓我們爲實際運用構造壹個 mixin。
例如,許多浏覽器對象的壹個重要功能是它們可以生成事件。事件是向任何有需要的人“廣播信息”的好方法。因此,讓我們構造壹個 mixin,使我們能夠輕松地將與事件相關的函數添加到任意 class/object 中。
Mixin 將提供 .trigger(name, [...data])
方法,以在發生重要的事情時“生成壹個事件”。name
參數(arguments)是事件的名稱,[...data]
是可選的帶有事件數據的其他參數(arguments)。
此外還有 .on(name, handler)
方法,它爲具有給定名稱的事件添加了 handler
函數作爲監聽器(listener)。當具有給定 name
的事件觸發時將調用該方法,並從 .trigger
調用中獲取參數(arguments)。
……還有 .off(name, handler)
方法,它會刪除 handler
監聽器(listener)。
添加完 mixin 後,對象 user
將能夠在訪客登錄時生成事件 "login"
。另壹個對象,例如 calendar
可能希望監聽此類事件以便爲登錄的人加載日曆。
或者,當壹個菜單項被選中時,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)); } };
.on(eventName, handler)
— 指定函數 handler
以在具有對應名稱的事件發生時運行。從技術上講,這兒有壹個用于存儲每個事件名稱對應的處理程序(handler)的 _eventHandlers
屬性,在這兒該屬性就會將剛剛指定的這個 handler
添加到列表中。
.off(eventName, handler)
— 從處理程序列表中刪除指定的函數。
.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
可以輕松地將此類行爲添加到我們想要的多個類中,並且不會影響繼承鏈。
Mixin —— 是壹個通用的面向對象編程術語:壹個包含其他類的方法的類。
壹些其它編程語言允許多重繼承。JavaScript 不支持多重繼承,但是可以通過將方法拷貝到原型中來實現 mixin。
我們可以使用 mixin 作爲壹種通過添加多種行爲(例如上文中所提到的事件處理)來擴充類的方法。
如果 Mixins 意外覆蓋了現有類的方法,那麽它們可能會成爲壹個沖突點。因此,通常應該仔細考慮 mixin 的命名方法,以最大程度地降低發生這種沖突的可能性。