深入淺出 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