混入

除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。 在Scala等语言里 mixins 及其特性已经很熟悉了,它在JavaScript中也是很流行的。

严格来说,mixin并不是面向对象编程的正式Struts。 但是,它们是将对象组合在一起的一种方法,可以帮助您“优先于继承而不是继承”。 它们有点像多重继承,因为它们使您可以将功能与单独的实现组合在一起。

在 TypeScript 中,可以根据不同的功能定义多个可复用的类,它们将作为 mixins。因为 extends 只支持继承一个父类,我们可以通过 implements 来连接多个 mixins,并且使用原型链连接子类的方法和父类的方法。

这就像组件拼合一样,由一堆细粒度的 mixins 快速搭建起一个功能强大的类。

先来看一个基础例子:

let target = {  a: 1,  b: 1 }
let source1 = {  a: 2,  c: 3 }
let source2 = {  b: 2,  d: 4 }
Object.assign(target, source1, source2)
console.log(target) // { a: 2, b: 2, c: 3, d: 4 }

通过 Object.assign()source1source2 混入到 target 上,并且替换了 target 对象原有的属性值。

TypeScript Mixins

先介绍一个前置知识: Object.getOwnPropertyNames() 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。

下面的代码演示了如何在 TypeScript 中使用混入:

// Disposable Mixin
class Disposable {
  isDisposed!: boolean
  dispose() {
    this.isDisposed = true
  }
}
// Activatable Mixin
class Activatable {
  isActive!: boolean;
  activate() {
    this.isActive = true
  }
  deactivate() {
    this.isActive = false
  }
}
class SmartObject{
  constructor() {
    setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500)
  }
  interact() {
    this.activate()
  }
  // Disposable
  isDisposed: boolean = false
  dispose!: () => void
  // Activatable
  isActive: boolean = false
  activate!: () => void
  deactivate!: () => void
}
applyMixins(SmartObject, [Disposable, Activatable])
let smartObj = new SmartObject()
setTimeout(() => smartObj.interact(), 2000)
function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach(baseCtor => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
      derivedCtor.prototype[name] = baseCtor.prototype[name]
    })
  })
}

代码里首先定义了两个类,它们将做为 mixins。可以看到每个类都只定义了一个特定的行为或功能 。稍后我们使用它们来创建一个新类,同时具有这两种功能。

// Disposable Mixin
class Disposable {
  isDisposed!: boolean
  dispose() {
    this.isDisposed = true
  }
}
// Activatable Mixin
class Activatable {
  isActive!: boolean
  activate() {
    this.isActive = true
  }
  deactivate() {
    this.isActive = false
  }
}

下面使用 implements 连接多个父类,需要在子类里实现所有接口定义。

class SmartObject implements Disposable, Activatable {}

这么做是为将要 mixin 进来的属性/方法创建出占位属性 。这告诉编译器这些成员在运行时是可用的,这样就能使用 mixin 带来的便利,虽说需要提前定义一些占位属性。

 // Disposable
  isDisposed: boolean = false
  dispose!: () => void
  // Activatable
  isActive: boolean = false
  activate!: () => void
  deactivate!: () => void

子类对外暴露一个封装后的 public 方法,方法的具体实现可以借助混入的 mixins 类中的属性/方法:

  interact() {
    this.activate()
  }

最后,把 mixins 混入定义的类,完成全部实现部分。

applyMixins(SmartObject, [Disposable, Activatable])

applyMixins() 方法借助 Object.getOwnPropertyNames() 遍历 mixins 上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach(baseCtor => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
      derivedCtor.prototype[name] = baseCtor.prototype[name]
    })
  })
}

applyMixins() 这个工具函数可以封装在项目中一个核心函数库中。

混入这种思想在一些开源项目如 materialvue-class-component 中被广泛使用,我们日常工作中也可以根据需求借鉴使用。

下一节:TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准。