Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

Mixins

除了传统的面向对象层次结构之外,另一种从可重用组件构建类的流行方式是通过组合更简单的部分类来构建它们。 你可能熟悉 Scala 等语言的 mixin 或 trait 的概念,并且这种模式在 JavaScript 社区中也变得流行起来。

Mixin 是如何工作的?

该模式依赖于将泛型与类继承结合使用来扩展基类。 TypeScript 对 mixin 的最佳支持是通过类表达式模式实现的。 你可以在此处阅读有关此模式在 JavaScript 中如何工作的更多信息。

首先,我们需要一个将应用 mixin 的基类:

ts
class 
Sprite
{
name
= "";
x
= 0;
y
= 0;
constructor(
name
: string) {
this.
name
=
name
;
} }
Try

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

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;
Property 'shouldFreeze' does not exist on type 'Player'.
// 运行时方面可以通过类型组合或接口合并手动复制。 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
; // string
Try