十、混入

除了传统的OO层次结构外,另一种流行的从可重用组件中建立类的方式是,通过组合更简单的部分类来建立它们。你可能对 Scala等语言的mixinstraits的想法很熟悉,这种模式在JavaScript社区也达到了一定的普及。

10.1 混入是如何工作的?

该模式依赖于使用泛型与类继承来扩展基类。TypeScript最好的mixin支持是通过类表达模式完成的。你可以在 这里阅读更多关于这种模式在JavaScript中的工作方式。

为了开始工作,我们需要一个类,在这个类上应用混入:

class Sprite {
name = "";
x = 0;
y = 0;

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

然后你需要一个类型和一个工厂函数,它返回一个扩展基类的表达式。

// 为了开始工作,我们需要一个类型,我们将用它来扩展其他类。
// 主要的责任是声明, 传入的类型是一个类。

type Constructor = new (...args: any[]) => {};

// 这个混集器增加了一个 `scale` 属性,并带有getters和setters
// 用来改变它的封装的私有属性。

function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// 混入不能声明私有/受保护的属性
// 但是,你可以使用ES2020的私有字段
_scale = 1;

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

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

有了这些设置,你就可以创建一个代表基类的类,并应用混合元素。

// 从Sprite类构成一个新的类。
// 用Mixin Scale应用程序:
const EightBitSprite = Scale(Sprite);

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

10.2 受约束的混入

在上述形式中,混入没有关于类的底层知识,这可能使它很难创建你想要的设计。

为了模拟这一点,我们修改了原来的构造函数类型以接受一个通用参数。

// 这就是我们之前的构造函数
type Constructor = new (...args: any[]) => {};
// 现在我们使用一个通用的版本,它可以在以下方面应用一个约束
// 该混入所适用的类
type GConstructor<T = {}> = new (...args: any[]) => T;

这允许创建只与受限基类一起工作的类。

type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;

然后,你可以创建混入函数,只有当你有一个特定的基础时,它才能发挥作用。

function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// 这个混合器只有在传递给基类的情况下才会起作用。
// 类中定义了setPos,因为有了可定位的约束。
this.setPos(0, 20);
}
};
}

10.3 替代模式

本文档的前几个版本推荐了一种编写混入函数的方法,即分别创建运行时和类型层次,然后在最后将它们合并:

// 每个mixin都是一个传统的ES类
class Jumpable {
jump() {}
}

class Duckable {
duck() {}
}

// 基类
class Sprite {
x = 0;
y = 0;
}

// 然后,你创建一个接口,
// 将预期的混合函数与你的基础函数同名,
// 合并在一起。
interface Sprite extends Jumpable, Duckable {}
// 在运行时,通过JS将混入应用到基类中
applyMixins(Sprite, [Jumpable, Duckable]);

let player = new Sprite();
player.jump();
console.log(player.x, player.y);

// 它可以存在于你代码库的任何地方
function applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null),
);
});
});
}

这种模式较少依赖于编译器,而更多地依赖于你的代码库,以确保运行时和类型系统都能正确地保持同步。

10.4 限制条件

mixin模式在TypeScript编译器中通过代码流分析得到了本地支持。在一些情况下,你会遇到本地支持的边界。

10.4.1 装饰器和混入 #4881

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

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

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

// 播放器类没有合并装饰器的类型
const player = new Player();
player.shouldFreeze;
// Ⓧ 属性'shouldFreeze'在类型'Player'上不存在

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

const playerTwo = new Player() as unknown as FreezablePlayer;
playerTwo.shouldFreeze;

10.4.2 静态属性混入 #17829

与其说是约束,不如说是一个难题。类表达式模式创建了单子,所以它们不能在类型系统中被映射以支持不同的变量类型。

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

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

特别声明: 本文转自 古艺散人老师 ,如有需要可前往原文预览查看。