除了传统的 OO 层次结构,另一种从可重用组件构建类的流行方法是通过组合更简单的部分类来构建它们。 您可能熟悉 Scala 等语言的 mixin 或特征的想法,并且该模式在 JavaScript 社区中也很流行。
模式依赖于使用具有类继承的泛型来扩展基类。 TypeScript 最好的 mixin 支持是通过类表达式模式完成的。
看一个例子:
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
type Constructor = new (...args: any[]) => {};
// This mixin adds a scale property, with getters and setters
// for changing it with an encapsulated private property:
function Scale(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;
setScale(scale: number) {
this._scale = scale;
}
get scale(): number {
return this._scale;
}
};
}
const EightBitSprite = Scale(Sprite);
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log('Ethan:' ,flappySprite.scale);
本例子和我之前的文章TypeScript 类装饰器的一个例子和使用单步调试搞清楚其运行原理其实很类似,只是没有使用装饰器语法罢了。
使用Scale 对 Sprite 进行装配,传入的是 Class Sprite 的定义:
返回的是一个新的函数,但只要不使用该函数去实例化新的类实例,函数体就不会执行。
现在准备使用 Scale 装饰过后的 Sprite 的扩展类去进行实例化操作了:
即将进入 mixin 内部:
Constrained Mixins
我们可以对上述 Mixins 做一些改造和增强。
在上面的形式中,mixin 没有类的基础知识,这会使创建你想要的设计变得困难。
比如,使用上面的 mixin,我们可以给任意的 Class 添加 _scale 属性。
如果我们想对此做进一步限制,比如限制 Scale 只能装饰某些特定类型的 Class.
为了对此建模,我们修改原始构造函数类型以接受泛型参数。
// This was our previous constructor:
type Constructor = new (...args: any[]) => {};
// Now we use a generic version which can apply a constraint on
// the class which this mixin is applied to
type GConstructor = new (...args: any[]) => T;
现在,使用这个类型构造器,必须传入一个基类的类型作为类型参数:
type Spritable = GConstructor;
现在,Scale 装饰器只能修饰 Sprite 及其子类了:
现在,如果传入一个并非 Sprite 及其子类的方法进入 Scale 装饰器,会引起语法错误:
另一种通过 Object.defineProperty 实现的 Mixin
// 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)
);
});
});
}
把 Jumpable 和 Duckable 的属性通过 Object.defineProperty 赋给 Sprite:
最后的运行时效果:
interface Sprite,可以使用 Duckable 和 Jumpable 类里定义的方法了。
更多Jerry的原创文章,尽在:"汪子熙":