在 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()
(在標記為 (*)
的行中)會在該混入的原型中尋找方法,而不是類別。
以下是圖表(請參閱右側)
這是因為方法 sayHi
和 sayBye
最初是在 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 的命名方法,以將發生的機率降至最低。
留言
<code>
標籤,若要插入多行,請將它們包在<pre>
標籤中,若要插入 10 行以上,請使用沙盒(plnkr、jsbin、codepen…)