在“函数式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);
总结
当我们要实现的功能需要现有类的参与,但又不想对现有类做任何修改时,使用这种纯函数式的方法组合出功能是非常适合的。这一思路的一种实现方式是通过继承获得子类,并将行为糅合到新创建的子类上。