Mixins
除了传统的面向对象继承体系,另一种流行的构建类的方式是通过组合较简单的部分类来构建它们。你可能熟悉类似Scala的混入(mixins)或特质(traits)的概念,这种模式也在JavaScript社区中变得流行起来。
Mixin工作原理
这种模式依赖于使用泛型和类继承来扩展基类。在TypeScript中,最好的混入支持是使用类表达式模式实现的。要开始使用混入模式,需要一个将应用混入的类作为基类:
class Sprite {
name = '';
x = 0;
y = 0;
constructor(name: string) {
this.name = name
}
}
之后需要一个类型和一个工厂函数,该工厂函数返回一个类表达式,该表达式继承了基类:
type Constructor = new (...args: any[]) => {}
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
_scale = 1;
set scale(scale: number) {
this._scale = scale
}
get scale(): number {
return this._scale
}
}
}
const EightBitSprite = Scale(Sprite)
const flappySprite = new EightBitSprite("Bird");
flappySprite.scale = 0.8
受限混入
在上述形式中,混入(mixin)并没有关于类的底层知识,这可能会使得创建所需的设计变得困难。
为了实现这个,可以修改原始构造函数类型,使其接受一个泛型参数:
type GConstructor<T = {}> = new (...args: any[]) => T
这可以用来创建只能与受限制基类配合使用的类:
class Position {
public x = 0;
public y = 0;
setPos(x: number, y: number) {
this.x = x;
this.y = y
}
}
type GConstructor<T = {}> = new (...args: any[]) => T
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void}>;
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
this.setPos(10, 20)
}
}
}
const PositionClass = Jumpable(Position)
const pos = new PositionClass()
console.log(pos);
替代模式
上述版本的示例建议了一种编写混入的方式,其中你可以分别创建运行时和类型层次结构,然后在最后将它们合并。
// Each mixin is a traditional ES class
class Jumpable {
jump() {}
}
class Duckable {
duck() {}
}
// Including the base
class Sprite {
x = 0;
y = 0;
}
// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);
let player = new Sprite();
player.jump();
console.log(player.x, player.y);
// This can live anywhere in your codebase:
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)
);
});
});
}
这种模式更少依赖于编译器,更多依赖于代码库来确保运行时和类型系统正确保持同步。
限制
TypeScript编译器通过代码流分析本地支持混合模式。这使得编译器能够分析和理解应用于类的混合模式的组合,从而实现准确的类型检查和推断。
- 不能使用装饰器通过代码流分析来提供混入模式。
// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
return class Pausable extends target {
shouldFreeze = false;
};
};
@Pausable
class Player {
x = 0;
y = 0;
}
// The Player class does not have the decorator's type merged:
const player = new Player();
// Property 'shouldFreeze' does not exist on type 'Player'.
player.shouldFreeze;
// The runtime aspect could be manually replicated via
// type composition or interface merging.
type FreezablePlayer = Player & { shouldFreeze: boolean };
const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;
- 与其说是限制,不如说是陷阱。类表达式模式创建单例,因此它们无法映射到类型系统以支持不同的变量类型。但可以通过使用函数返回基于泛型而不同的类来解决此问题:
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>() {}