1. 程式人生 > >ES2015 中的函式式Mixin

ES2015 中的函式式Mixin

原文連結http://raganwald.com/2015/06/17/functional-mixins.html

在“原型即物件”中,我們看到可以對原型使用 Object.assign 來模擬 mixin,原型是 JavaScript 中類概念的基石。現在我們將回顧這個概念,並進一步探究如何將功能糅合進類。

首先,簡單回顧一下:在 JavaScript 中,類是通過一個建構函式和它的原型來定義的,無論你是用 ES5 語法,還是使用 class 關鍵字。類的例項是通過 new 呼叫構造器的方式建立的。例項從構造器的 prototype 屬性上繼承共享的方法。

物件 mixin 模式

如果多個類共享某些行為,或者希望對龐雜的原型物件進行功能提取,這時候就可以使用 mixin 來對原型進行擴充套件。 
  如這裡的 Todo 類
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}) {
    
this.colourCode = {r, g, b}; return this; }, getColourRGB () { return this.colourCode; } };

  將顏色編碼功能糅合到 Todo 原型上是簡而易行的
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 而專門做此實現嗎?很可能不需要,因為自己實現一套多型機制是不得已而為之的做法。但這一點使得編寫測試用例很順手,並且一些激進的框架開發者可能在函式的多分派和模式匹配上求索,或許會用上這一點。  

總結

物件 mixin 的迷人之處在於簡單:它不需要在物件字面值和 Object.assign 之上做一層抽象。   然而,通過 mixin 模式定義的行為和通過 class 關鍵字定義的行為存在些許差異。體現差異的兩個例子分別是可列舉性以及 mixin 自身的屬性(如常量和類似於 [Symbol.hasInstance] 的 mixin 自身方法)。   函式式 mixin 使得實現上述功能成為可能,不過生成函式式 mixin 的 FunctionalMixin 函式引入了一定複雜性。   一般來說,最好保持同一個問題域下的程式碼表現儘可能相似,而這有時不可避免地增加基礎程式碼的複雜性。但這一點更多的是一種指導思想,而非需要恪守的萬能教條。出於此,物件 mixin 模式和函式式 mixin 在 JavaScript 中都有各自的一席之地