ES2015 中的函式式Mixin
阿新 • • 發佈:2018-11-27
原文連結:http://raganwald.com/2015/06/17/functional-mixins.html
在“原型即物件”中,我們看到可以對原型使用 Object.assign 來模擬 mixin,原型是 JavaScript 中類概念的基石。現在我們將回顧這個概念,並進一步探究如何將功能糅合進類。
首先,簡單回顧一下:在 JavaScript 中,類是通過一個建構函式和它的原型來定義的,無論你是用 ES5 語法,還是使用 class 關鍵字。類的例項是通過 new 呼叫構造器的方式建立的。例項從構造器的 prototype 屬性上繼承共享的方法。
物件 mixin 模式
如果多個類共享某些行為,或者希望對龐雜的原型物件進行功能提取,這時候就可以使用 mixin 來對原型進行擴充套件。class Todo { constructor (name) { this.name = name || 'Untitled'; this.done = false; } do () { this.done = true; return this; } undo () { this.done = false; return this; } }用於顏色編碼的 mixin 如下:
const Coloured = { setColourRGB ({r, g, b}) {將顏色編碼功能糅合到 Todo 原型上是簡而易行的:this.colourCode = {r, g, b}; return this; }, getColourRGB () { return this.colourCode; } };
Object.assign(Todo.prototype, Coloured); new Todo('test') .setColourRGB({r: 1, g: 2, b: 3}) //=> {"name":"test","done":false,"colourCode":{"r":1,"g":2,"b":3}}我們還可以升級為使用私有屬性:
const colourCode = Symbol("colourCode"); const Coloured = { setColourRGB ({r, g, b}) { this[colourCode]= {r, g, b}; return this; }, getColourRGB () { return this[colourCode]; } };至此,非常簡單明瞭。我們將這稱為一種 “模式”,像菜譜一樣,是解決某種問題的獨特的程式碼組織方式。
函式式 mixin
上面的物件 mixin 功能完好,但用它來解決問題要分兩步走:定義 mixin 然後擴充套件類的原型。Angus Croll 指出將 mixin 定義成函式而不是物件會是更優雅的做法,並稱之為函式式 mixin。再次以 Coloured 為例,將它改寫成函式的形式:const Coloured = (target) => Object.assign(target, { setColourRGB ({r, g, b}) { this.colourCode = {r, g, b}; return this; }, getColourRGB () { return this.colourCode; } }); Coloured(Todo.prototype);我們可以定義一個工廠函式,並從命名上體現該模式:
const FunctionalMixin = (behaviour) =>
target => Object.assign(target, behaviour);
現在我們可以精要地定義函式式 mixin:
const Coloured = FunctionalMixin({ setColourRGB ({r, g, b}) { this.colourCode = {r, g, b}; return this; }, getColourRGB () { return this.colourCode; } });
可列舉性
如果我們探究 class 宣告類的方式下對 prototype 的操作,可以發現宣告的方法預設是不可列舉的。這可以避免一個常見問題——遍歷例項的 key 時程式設計師有時忘記檢測 .hasOwnProperty。 而我們的物件 mixin 模式無法做到這點,定義在 mixin 中的方法預設是可列舉的。如果我們故意將其設定為不可列舉,Object.assign 就不會將它們糅合到目標原型了,因為 Object.assign 只會將可列舉的屬性賦值到目標物件上。 這將導致以下情形:Coloured(Todo.prototype) const urgent = new Todo("finish blog post"); urgent.setColourRGB({r: 256, g: 0, b: 0}); for (let property in urgent) console.log(property); // => name done colourCode setColourRGB getColourRGB正如所見,setColourRGB 和 getColourRGB 方法被枚舉出來了,而 do 和 undo 方法卻沒有。這對於不健壯的程式碼會是個問題,因為我們不可能每次都重寫別處的程式碼,處處加上 hasOwnProperty 檢測。 該問題使用函式式 mixin 便可迎刃而解,我們可以神乎其神地讓 mixin 表現得和 class 宣告類似,這是函式式 mixin 的好處之一:
const FunctionalMixin = (behaviour) => function (target) { for (let property of Reflect.ownKeys(behaviour)) Object.defineProperty(target, property, { value: behaviour[property] }) return target; }將上面 mixin 的主體部分作為一種程式碼模板一遍遍寫出來不但累人而且容易出錯,而將其封裝到函式裡則是一種小進步。
mixin 的職責
和類一樣,mixin 是元物件:它們給例項定義行為。除了以方法的形式定義物件的行為,類還負責初始化例項。有的時候,類和元物件還會具有其他的功能。 例如,有時某個概念涉及到一組人盡皆知的常量。如果使用類,那麼將這些常量定義在 class 本身上則非常方便,這時 class 本身充當了名稱空間的作用。class Todo { constructor (name) { this.name = name || Todo.DEFAULT_NAME; this.done = false; } do () { this.done = true; return this; } undo () { this.done = false; return this; } } Todo.DEFAULT_NAME = 'Untitled'; // If we are sticklers for read-only constants, we could write: // Object.defineProperty(Todo, 'DEFAULT_NAME', {value: 'Untitled'});我們無法使用 “簡單 mixin” 做同樣的事,因為預設情況下,“簡單 mixin” 的所有屬性最終都被糅合到例項的 prototype 上。例如,我們想定義 Coloured.RED, Coloured.GREEN, Coloured.BLUE,但我們並不想在任何例項個體上定義 RED, GREEN, BLUE。 同樣,我們可以藉助函式式 mixin 來解決該問題。FunctionalMixin 工廠函式將接收一個可選的字典,該字典包含只讀的 mixin 屬性,該字典通過一個特殊的鍵給出:
const shared = Symbol("shared"); function FunctionalMixin (behaviour) { const instanceKeys = Reflect.ownKeys(behaviour) .filter(key => key !== shared); const sharedBehaviour = behaviour[shared] || {}; const sharedKeys = Reflect.ownKeys(sharedBehaviour); function mixin (target) { for (let property of instanceKeys) Object.defineProperty(target, property, { value: behaviour[property] }); return target; } for (let property of sharedKeys) Object.defineProperty(mixin, property, { value: sharedBehaviour[property], enumerable: sharedBehaviour.propertyIsEnumerable(property) }); return mixin; } FunctionalMixin.shared = shared;現在我們便可以這樣寫:
const Coloured = FunctionalMixin({ setColourRGB ({r, g, b}) { this.colourCode = {r, g, b}; return this; }, getColourRGB () { return this.colourCode; }, [FunctionalMixin.shared]: { RED: { r: 255, g: 0, b: 0 }, GREEN: { r: 0, g: 255, b: 0 }, BLUE: { r: 0, g: 0, b: 255 }, } }); Coloured(Todo.prototype) const urgent = new Todo("finish blog post"); urgent.setColourRGB(Coloured.RED); urgent.getColourRGB() //=> {"r":255,"g":0,"b":0}
mixin 本身的方法
JavaScript 中屬性未必是值(和函式相對)。有時候,類也具有方法。同樣,有時 mixin 具有自己的方法也是合理的,比如當涉及到 instanceof 時。 在 ECMAScript 的之前版本中,instanceof 操作符檢查例項的 prototype 是否和建構函式的 prototype 相匹配。它和“類”配合使用沒啥問題,但卻無法直接和 mixin 協同工作。urgent instanceof Todo //=> true urgent instanceof Coloured //=> false這是 mixin 存在的問題。另外,程式設計師可能根據需要建立動態型別,或者直接使用 Object.create 和 Object.setPrototypeOf 管理原型,它們都可能導致 instanceof 工作不正常。ECMAScript 2015 提供了一種方式來覆寫內建的 instanceof 的行為,即物件可以定義一個特殊的方法,該方法屬性的名字是一個既定的符號——Symbol.hasInstance。 我們可以簡單測試一下:
Object.defineProperty(Coloured, Symbol.hasInstance, {value: (instance) => true}); urgent instanceof Coloured //=> true {} instanceof Coloured //=> true當然,上面的例子在語義上是不對的。然而藉助該技術,我們可以這樣做:
const shared = Symbol("shared"); function FunctionalMixin (behaviour) { const instanceKeys = Reflect.ownKeys(behaviour) .filter(key => key !== shared); const sharedBehaviour = behaviour[shared] || {}; const sharedKeys = Reflect.ownKeys(sharedBehaviour); const typeTag = Symbol("isA"); function mixin (target) { for (let property of instanceKeys) Object.defineProperty(target, property, { value: behaviour[property] }); target[typeTag] = true; return target; } for (let property of sharedKeys) Object.defineProperty(mixin, property, { value: sharedBehaviour[property], enumerable: sharedBehaviour.propertyIsEnumerable(property) }); Object.defineProperty(mixin, Symbol.hasInstance, {value: (instance) => !!instance[typeTag]}); return mixin; } FunctionalMixin.shared = shared; urgent instanceof Coloured //=> true {} instanceof Coloured //=> false你需要為了照顧 instanceof 而專門做此實現嗎?很可能不需要,因為自己實現一套多型機制是不得已而為之的做法。但這一點使得編寫測試用例很順手,並且一些激進的框架開發者可能在函式的多分派和模式匹配上求索,或許會用上這一點。