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

ES2015 中的函數式Mixin

實現 工廠 default wal 但我 簡單 所有 java neu

原文鏈接: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 中都有各自的一席之地

ES2015 中的函數式Mixin