Mixins
除了传统的面向对象层次结构之外,另一种从可重用组件构建类的流行方式是通过组合更简单的部分类来构建它们。 你可能熟悉 Scala 等语言的 mixin 或 trait 的概念,并且这种模式在 JavaScript 社区中也变得流行起来。
Mixin 是如何工作的?
该模式依赖于将泛型与类继承结合使用来扩展基类。 TypeScript 对 mixin 的最佳支持是通过类表达式模式实现的。 你可以在此处阅读有关此模式在 JavaScript 中如何工作的更多信息。
首先,我们需要一个将应用 mixin 的基类:
然后你需要一个类型和一个工厂函数,该函数返回一个扩展基类的类表达式。
ts
// 首先,我们需要一个用于扩展其他类的类型。
// 主要职责是声明传入的类型是一个类。
type Constructor = new (...args: any[]) => {};
// 这个 mixin 添加了一个 scale 属性,带有 getter 和 setter
// 用于通过封装的私有属性来改变它:
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins 不能声明 private/protected 属性
// 但是,你可以使用 ES2020 私有字段
_scale = 1;
setScale(scale: number) {
this._scale = scale;
}
get scale(): number {
return this._scale;
}
};
}Try设置好这些之后,你就可以创建一个表示应用了 mixin 的基类的类:
ts
// 从 Sprite 类组合一个新类,
// 并应用 Mixin Scale:
const EightBitSprite = Scale(Sprite);
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);Try约束 Mixin
在上述形式中,mixin 对底层类没有了解,这可能会使创建你想要的设计变得困难。
为了对此建模,我们修改原始构造函数类型以接受泛型参数。
ts
// 这是我们之前的构造函数:
type Constructor = new (...args: any[]) => {};
// 现在我们使用一个泛型版本,它可以对应用此 mixin 的类施加约束
type GConstructor<T = {}> = new (...args: any[]) => T;Try这允许创建仅适用于受约束基类的类:
ts
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;Try然后你可以创建仅在你拥有特定基类时才起作用的 mixin:
ts
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// 这个 mixin 只有在传入的基类
// 由于 Positionable 约束而定义了 setPos 时才能工作。
this.setPos(0, 20);
}
};
}Try替代模式
本文档的先前版本推荐了一种编写 mixin 的方式,即分别创建运行时和类型层次结构,然后在最后合并它们:
ts
// 每个 mixin 都是一个传统的 ES 类
class Jumpable {
jump() {}
}
class Duckable {
duck() {}
}
// 包括基类
class Sprite {
x = 0;
y = 0;
}
// 然后你创建一个接口,将预期的 mixin
// 与基类同名合并
interface Sprite extends Jumpable, Duckable {}
// 通过运行时 JS 将 mixin 应用到基类中
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)
);
});
});
}Try这种模式较少依赖编译器,而更多地依赖你的代码库来确保运行时和类型系统正确同步。
约束
mixin 模式通过代码流分析在 TypeScript 编译器内部得到原生支持。 在某些情况下,你可能会触及原生支持的边缘。
装饰器与 Mixins #4881
你不能使用装饰器通过代码流分析提供 mixin:
ts
// 一个复制 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;
// 运行时方面可以通过类型组合或接口合并手动复制。
type FreezablePlayer = Player & { shouldFreeze: boolean };
const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;Try静态属性 Mixins #17829
与其说是约束,不如说是一个陷阱。 类表达式模式创建单例,因此它们无法在类型系统中映射以支持不同的变量类型。
你可以通过使用函数返回基于泛型不同的类来解决这个问题:
ts
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; // stringTry