在“函数式mixin”中,我们讨论了将功能糅合到 JavaScript 类中。糅合功能的这一行为属于对类的修改。该做法在其他语言中也有对应的说法,如 Ruby 的模块。采用这一思路,我们可以将类分解成更小的实体,每个实体聚焦在单一的功能上,并且可以按需在多个类间共享。
 
话虽如此,对类进行修改也有一些缺点。人们常说:“如果代码会造成数据改动,那么分析解读它们就会比较困难”。当对类进行修改时,确实如此。
 
类通常是全局可见的。经验上看,某处对类的修改可能导致其他看上去不相关的部分功能受损,因为那部分程序并不期望类的这种改动。
 
当然了,如果改动只发生在类被创建的过程中(译者注:这里指类未被已有代码使用前,包括时间和空间两个维度),那就不存在这种顾虑了。但是如果我们想修改在别处定义的类怎么办呢?如果我们想将引起改动的代码聚集在一处又该如何?
 

继承

让我们回顾一下单纯至极的 Todo 类:
class Todo {
  constructor (name) {
    this.name = name || 'Untitled';
    this.done = false;
  }
  do () {
    this.done = true;
    return this;
  }
  undo () {
    this.done = false;
    return this;
  }
}

现在我们假设 Todo 类在整个应用程序中用途很多且多处使用。在某部分代码中,我们希望 Todo 项是有颜色的。正如前面章节看到的,可以使用简单 mixin 实现如下:

const Coloured = {
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  },
  getColourRGB () {
    return this.colourCode;
  }
};

Object.assign(Todo.prototype, Coloured);
尽管这样确实满足了这部分程序的要求,但也可能导致其他地方的 Todo 实例表现异常。实际上我们希望的是在这部分代码里使用 ColoredTodo,而在其他部分使用 Todo。
 
使用 extends 关键字就可以解决这个小案例的问题: 
class ColouredTodo extends Todo {
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  }
  getColourRGB () {
    return this.colourCode;
  }
}
ColouredTodo 和 Todo 几乎一样,不过是增加了颜色相关的方法。
 
 

共享是暖心之举

上面的问题使用继承得到了解决,然而这样做很难和其他类共享“颜色”功能,这是继承常被提到的缺陷。继承使类之间具有严格的树形结构关系。另一个缺陷是,对“颜色”功能的测试总是和 Todo 类连带着,而如果是设计良好的 mixin 则可以方便地独立测试。 
 
总而言之,继承带来的问题是颜色功能和 Todo 类耦合了,而使用 mixin 则不会;但使用 mixin 又会出现 Todo 类和 Coloured 相耦合,继承则无此问题。
 
我们想实现的目的是,通过继承让 Todo 不和 Coloured 绑在一起,通过 mixin 让 Coloured 不和 ColouredTodo 绑在一起。 
class ColouredTodo extends Todo {}

const Coloured = {
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  },
  getColourRGB () {
    return this.colourCode;
  }
};

Object.assign(ColouredTodo.prototype, Coloured);
可以编写一个简单函数来封装这一模式: 
function ComposeWithClass(clazz, ...mixins) {
  const subclazz = class extends clazz {};
  for (let mixin of mixins) {
    Object.assign(subclazz.prototype, mixin);
  }
  return subclazz;
}

const ColouredTodo = ComposeWithClass(Todo, Coloured);
ComposeWithClass 函数返回了一个新的类,而没有修改它接收的参数。换句话说,它使用类来组合出功能,而不是将功能糅合进类。
 

功能增强 

我们可以进一步丰富 ComposeWithClass 的功能,解决掉在探究 mixin 时发现的相同问题,比如可以让方法不可枚举: 
const shared = Symbol("shared");

function ComposeWithClass(clazz, ...mixins) {
  const subclazz = class extends clazz {};

  for (let mixin of mixins) {
    const instanceKeys = Reflect
      .ownKeys(mixin)
      .filter(key => key !== shared && key !== Symbol.hasInstance);
    const sharedBehaviour = mixin[shared] || {};
    const sharedKeys = Reflect.ownKeys(sharedBehaviour);

    for (let property of instanceKeys)
      Object.defineProperty(subclazz.prototype, property, { value: mixin[property] });
    for (let property of sharedKeys)
      Object.defineProperty(subclazz, property, {
        value: sharedBehaviour[property],
        enumerable: sharedBehaviour.propertyIsEnumerable(property)
      });
  }
  return subclazz;
}

ComposeWithClass.shared = shared;

按照以上实现,每个 mixin 自己可以决定使用何属性提供对 instanceof 的支持。

const isaColoured = Symbol();

const Coloured = {
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  },
  getColourRGB () {
    return this.colourCode;
  },
  [isaColoured]: true,
  [Symbol.hasInstance] (instance) { return instance[isaColoured]; }
};

也可以对此进行封装:

function HasInstances (behaviour) {
  const typeTag = Symbol();
  return Object.assign({}, behaviour, {
    [typeTag]: true,
    [Symbol.hasInstance] (instance) { return instance[typeTag]; }
  })
}
完整的复合示例: 
class Todo {
  constructor (name) {
    this.name = name || 'Untitled';
    this.done = false;
  }
  do () {
    this.done = true;
    return this;
  }
  undo () {
    this.done = false;
    return this;
  }
}

const Coloured = HasInstances({
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  },
  getColourRGB () {
    return this.colourCode;
  }
});

const ColouredTodo = ComposeWithClass(Todo, Coloured);
 

总结 

当我们要实现的功能需要现有类的参与,但又不想对现有类做任何修改时,使用这种纯函数式的方法组合出功能是非常适合的。这一思路的一种实现方式是通过继承获得子类,并将行为糅合到新创建的子类上。
11-29 14:11