Skip to content
目录

除了传统的面向对象层次结构,从可重用组件构建类的另一种流行方法是通过组合更简单的部分类来构建类。你可能熟悉Scale类似语言的mixins或traits特性,这种模式在JavaScript中也很流行。

1️⃣ Mixin是如何工作的?

该模式依赖于使用泛型和类继承来扩展基类。TypeScript通过类表达式模式对mixin提供最佳支持。你可以阅读 这里 了解该模式如何在JavaScript中运作。

首先,我们需要一个将mixins应用到该类上:

typescript
class Sprite {
  name = '';
  x = 0;
  y = 0;

  constructor(name: string) {
    this.name = name;
  }
}

然后你需要一个类型,和一个继承基类的类表达式(即 返回另一个类的高阶函数):

typescript
// 首先,我们需要一个类型,用于被其它类扩展
// 🤩这里的 Constructor 最要用于约束传入的参数是一个类
type Constructor = new (...args: any[]) => {};

// 这个 mixin 添加一个 scale 属性
// 使用getters & setters 改变封装的私有属性
function Scale<TBase extends Constructor>(Base: TBase) {
  return class Scaling extends Base {
    // Mixins 可能不会声明 private|protected 属性
    // 但是你可以使用ES2020私有字段 即  #scale 的形式
    _scale = 1;

    _setScale(scale: number) {
      this._scale = scale;
    }

    get scale() {
      return this._scale;
    }
  }
}

设置好这一切之后,你就可以创建一个应用了基类的Mixin的类:

typescript
// 通过Mixin Scale 从Sprite类构成一个新的类
const EightBitSprite = Scale(Sprite)

const flappySprite = new EightBitSprite('Bird')
flappySprite.setScale(0.8)
console.log(flappySprite.scale)

2️⃣ 约束Mixins

上面的形式,mixin对类的底层一无所知,这对创建你想要设计的类不太友好。

为了添加模型,我们修改原来的构造器,现在它接收一个泛型的参数:

typescript
// 先前的构造器
type Constructor = new (...args: any[]) => {};

// 现在需要一个泛型版本
// 对需要mixin的类进行约束
type GConstructor<T> = new (...args: any[]) => T;

这使我们创建的类必须满足某种特殊约束的基类才行😎:

typescript
// Mixin的基类必须包含 setPos 函数,且函数签名也要满足
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void}>;

// Mixin的基类必须是Sprite类型
type Spritable = GConstructor<Sprite>;

type Loggable = GConstructor<{ print: () => void}>;

然后,你可以在特定的基类智商创建Mixins:

typescript
// 💡基类必须满足 Positionable 的约束
function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      // 基类上的 setPos方法🤩
      this.setPos(0, 20)
    }
  }
}

3️⃣ 🚀 另一种替换的模式

本文档之前的版本推荐了一种编写mixin的方法,你可以分别创建运行时和类型层次结构,然后在最后合并它们:

typescript
// 每种Mixin都是传统的ES class
class Jumpable {
  jump() {}
}

class Duckable {
  duck() {}
}

class Sprite {
  x = 0;
  y = 0;
}

// 💡然后创建一个接口,将预期的mixins与基类名称相同的mixins合并在一起
interface Sprite extends Duckable, Jumpable {}

// 🤩通过JS运行时将Mixins应用到base上
applyMixins(Sprite, [Jumpable, Duckable])

let player = new Sprite()
player.jump() // 通过mixin,player拥有了 Jumpable的功能
console.log(player.x, player.y)

// 这个工具函数可以放到代码的任何地方
function applyMixins(derivedCtor, constructors: any[]) {
  constructors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
        	|| Object.create(null)
      )
    })
  })
}

TIP

关于 applyMixins 中知识点:

  1. Object.getOwnPropertyNames() 列举出自身字符串属性keys,这里主要是 constructor & name 2个keys
  2. Object.defineProperty() 给对象添加属性值,这里主要是将基类的constructor赋值到派生类上,进行浅拷贝
  3. Object.getOwnPropertyDescriptor() 获取属性描述符,这里主要将基类上属性添加到派生类上
  4. Object.create() 创建一个新的对象,这里创建一个空的对象

这个模式对编译器依赖更少,更多依赖你的代码,确保运行时和类型系统保持同步。

4️⃣ 限制

Mixin模式被TypeScript编译器通过代码流原生支持。有些场景可能会触碰到原生支持的边界😅。

4.1 装饰器和Mixins

可查看 issuses@4881

你不能使用装饰器通过代码流分析来提供mixin:

typescript
// 一个复制Mixin模式的装饰器函数
const Pausable = (target: typeof Player) => {
  return class Pausable extends target {
    shouldFreeze = false
  }
}

@Pausable
class Player {
  x = 0;
  y = 0;
}

// 🚨Player类不会将装饰器类型进行合并
const player = new Player()
player.shouldFreeze;
// ❌ 属性 'shouldFreeze' 不存在于类型 'Player' 上

// 💡 运行时可以通过类型组合或接口合并可以手动复制
type FreezablePlayer = Player & { shouldFreeze: boolean }

const player2 = (new Player() as unknown) as FreezablePlayer;
playe2.shouldFreeze; // 👌

TIP

使用装饰器,需要在 tsconfig.json 中开启下面编译选项:

json
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

4.2 静态属性Mixins

可查看 issues@17829

这个更像一个缺陷,而不是限制。类表达式模式创建单例,因此它们不能在类型系统中被映射以支持不同的变量类型。

你可通过使用函数返回基于泛型不同的类来解决这个问题:

typescript
function base<T>() {
  class Base {
    static prop: T
  }
  return Base
}

function derived<T>() {
  class Derived extends base<T>() {
    static anotherProp: T
  }
  return Derived
}

class Spec extends derived<string>() {}

Spec.prop; // string
Spec.anotherProp; // string

原文档:

TIP

译者注:

Mixins模式在之前的JS库中很常见,比如老版本的React和老版本的Vue中,都能看到。它本质上就是对已有的功能进行扩展,但现在一般通过组合的模式进行功能扩展,更加的灵活。

2022年09月17日17:46:38