TypeScript Office - 混入
# 混入
除了传统的 OO 层次结构外,另一种流行的从可重用组件中建立类的方式是,通过组合更简单的部分类来建立它们。你可能对 Scala 等语言的 mixins 或 traits 的想法很熟悉,这种模式在 JavaScript 社区也达 到了一定的普及。
# 混入是如何工作的?
该模式依赖于使用泛型与类继承来扩展基类。TypeScript 最好的 mixin 支持是通过类表达模式完成的。你可以在 这里 (opens new window) 阅读更多关于这种模式在JavaScript中的工作方式。
为了开始工作,我们需要一个类,在这个类上应用混入:
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
然后你需要一个类型和一个工厂函数,它返回一个扩展基类的表达式。
// 为了开始工作,我们需要一个类型,我们将用它来扩展其他类。
// 主要的责任是声明, 传入的类型是一个类。
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;
}
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
有了这些设置,你就可以创建一个代表基类的类,并应用混合元素。
// 从Sprite类构成一个新的类。
// 用Mixin Scale应用程序:
const EightBitSprite = Scale(Sprite);
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);
1
2
3
4
5
6
2
3
4
5
6
# 受约束的混入
在上述形式中,混入没有关于类的底层知识,这可能使它很难创建你想要的设计。
为了模拟这一点,我们修改了原来的构造函数类型以接受一个通用参数。
// 这就是我们之前的构造函数
type Constructor = new (...args: any[]) => {};
// 现在我们使用一个通用的版本,它可以在以下方面应用一个约束
// 该混入所适用的类
type GConstructor<T = {}> = new (...args: any[]) => T;
1
2
3
4
5
2
3
4
5
这允许创建只与受限基类一起工作的类。
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
1
2
3
2
3
然后,你可以创建混入函数,只有当你有一个特定的基础时,它才能发挥作用。
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// 这个混合器只有在传递给基类的情况下才会起作用。
// 类中定义了setPos,因为有了可定位的约束。
this.setPos(0, 20);
}
};
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 替代模式
本文档的前几个版本推荐了一种编写混入函数的方法,即分别创建运行时和类型层次,然后在最后将它们合并:
// 每个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)
);
});
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
这种模式较少依赖于编译器,而更多地依赖于你的代码库,以确保运行时和类型系统都能正确地保持同步。
# 限制条件
mixin 模式在 TypeScript 编译器中通过代码流分析得到了本地支持。在一些情况下,你会遇到本地支持的边界。
# 装饰器和混入
你不能使用装饰器来通过代码流分析提供混入:
// 一个复制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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 静态属性混入
与其说是约束,不如说是一个难题。类表达式模式创建了单子,所以它们不能在类型系统中被映射以支持不同的变量类型。
你可以通过使用函数返回你的类来解决这个问题,这些类基于泛型而不同:
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
编辑此页 (opens new window)
更新时间: 2023/09/18, 16:34:13