深入淺出 JavaScript 之 Mixin
實務上常會發現需要兩個物件的 method,但礙於 JavaScript 只能 單一繼承
於 prototype,我們無法同時繼承兩個物件;但透過 Mixin,我們可實現類似 多重繼承
的功能。
Version
ECMAScript 2015
Definition
Mixin
將物件中所有 method 複製到其他物件,讓該物件馬上擁有新的 method
實務上我們可能會想要 code reuse 其他物件的 method,直覺會想到 繼承
,但:
- JavaScript 只能單一繼承於 prototype,若我想要 code reuse 到兩個以上的物件呢?
- 根據
里氏替換原則
:父類別能被子類別取代
,也就是我們該以多型
為前提使用繼承,而不該以code reuse
使用繼承
但實務上的確有 code reuse 的需求,既然不能用 繼承
,我們該用什麼呢 ?
答案就是:Mixin
Object Mixin
Mixin1.js
1 | const CircleMixin = { |
第 1 行
1 | const CircleMixin = { |
宣告 CircleMixin
,其本質為 object,擁有 area()
method。
第 7 行
1 | const LogMixin = { |
宣告 LogMixin
,其本質亦為 object,擁有 startLog()
與 stopLog()
method。
17 行
1 | const Button = function (radius) { |
宣告 Button
constructor function,也可使用 ECMAScript 2015 的 class
與 constructor
。
Class 與 construcor function 本質相同,只是 syntax sugar
27 行
1 | Object.assign(Button.prototype, CircleMixin, LogMixin); |
如今我們希望 Button
class 同時有 CircleMixin
的 area()
,又有 LogMixin
的 startLog()
與 stopLog()
。
若使用繼承,JavaScript 無法同時繼承 CircleMixin
與 LogMixin
。
且 Button
無論繼承 CircleMixin
與 LogMixin
都違反 里氏替換原則
,因為 Button
並非 CircleMixin
或 LogMixin
多型體系下的成員。
但透過 Object.assign()
,我們能輕易將 CircleMixin
與 LogMixin
的所有 method 複製到 Button.prototype
,讓 Button
class 瞬間有了新的 method。
28 行
1 | // Button.prototype = {...Button.prototype, ...CircleMixin, ...LogMixin}; |
亦可使用 ECMAScript 2015 的 …
object spread operator,將所有物件 property 展開,重新合併重新的物件給 Button.prototype
。
30 行
1 | const button = new Button(5); |
經過 Mixin 之後,button
物件就有了 startLog()
、area()
與 stopLog()
三個 method,重點還是來自於不同的 Mixin 物件。
我們可發現 Mixin 為 object,以 Object Composition 方式組合出新功能,與 GoF 所謂的
多用組合,少用繼承
想法不謀而合,同時也解決了單一繼承
與里氏替換原則
所面臨的挑戰
Class Mixin
Mixin2.js
1 | const CircleMixin = base => class extends base { |
第 1 行
1 | const CircleMixin = base => class extends base { |
宣告 CircleMixin
,其本質為 function,回傳擁有 area()
的 class。
第 7 行
1 | const LogMixin = base => class extends base { |
宣告 LoginMixin
,其本質為 function,回傳擁有 startLog()
與 stopLog()
的 class。
17 行
1 | class Base {} |
extends
透過 CircleMixin()
與 LogMixin()
,達成類似 多重繼承
的效果。
語法上雖然看似
繼承
,實則為 Class Composition,與 Object Mixin 差異在於:
- Object Mixin 是建立 object 後再
組合
object- Class Mixin 是先組合 class 再建立 object
26 行
1 | const button = new Button(5); |
對 client 而言,使用 Object Mixin 與 Class Mixin ,用起來都一樣。
Functional Mixin
Mixin3.js
1 | const CircleMixin = function () { |
第 1 行
1 | const CircleMixin = function () { |
將 CircleMixin
由 object 改成 function。
使用 this.area
宣告物件的 method。
第 7 行
1 | const LogMixin = function () { |
將 LoginMixin
由 object 改成 function。
使用 this.startLog
與 this.stopLog
宣告物件的 method。
17 行
1 | const Button = function (radius) { |
宣告 Button
constructor function,也可使用 ECMAScript 2015 的 class
與 constructor
。
27 行
1 | CircleMixin.call(Button.prototype); |
在 CircleMixin()
與 LoginMixin()
中都使用了 this
,在 JavaScript 中,最重要的就是 this
到底是誰 ?
我們使用 Function.call()
將 this
指向 Button.prototype
,因此 area()
、startLog()
與 stopLog()
就自然成為 Button
的 method。
30 行
1 | const button = new Button(5); |
對 client 而言,無論使用 Object Mixin 、Class Mixin 或 Functional Mixin,用起來都一樣。
Functional Mixin 使用了
this
,因此必須搭配Function.call()
指定this
為何物件
Summary
Q:Mixin 的價值何在?
- 解決
單一繼承
所面臨的難題 - 解決
里氏替換原則
將繼承
用在多型
的限制
Q:Mixin 實務上該用在哪裡 ?
- 單純為了 code reuse,且之間並沒有
多型
的關係,因此不適合使用繼承
- 需要實現 Composition
Conclusion
- 有別於
繼承
,Mixin 提供了以 Composition 為基礎的解決方案 - Functional Mixin 需要有
this
與call
觀念,門檻較高
Sample Code
完整的範例可以在我的 GitHub 上找到
Reference
Angus Croll, A fresh look at JavaScript Mixins
MDN web docs, Classes