混入类型(Mixins)
你可能在 Scala 等语言里对mixins及trait已经很熟悉了,这种模式在 JavaScript 中也是很流行的。
在 TypeScript 中,可以根据不同的功能定义多个可复用的类,然后组合(或者扩展)这些可复用的类,从而快速搭建起一个功能强大的类。这就是mixins模式。
组合(或者扩展)可复用类,常借助extends、implements。extends只支持继承一个父类,implements可以连接多个可复用的类,并且使用原型链连接子类的方法和父类的方法。
Mixin 如何工作?
该模式依赖于使用具有类继承的泛型来扩展基类。TypeScript 最好的 mixin 支持是通过类表达式模式实现的。
开始,我们需要一个基类,在基类的基础上应用 mixin:
class Sprite { name = ""; x = 0; y = 0;constructor (name: string) { this.name = name; } }
然后需要一个类型(type)和一个工厂函数(factory function),它返回一个扩展基类的类表达式。
// To get started, we need a type which we'll use to extend // other classes from. The main responsibility is to declare // that the type being passed in is a class.type Constructor = new (...args: any[]) => {}; // This mixin adds a scale property, with getters and setters // for changing it with an encapsulated private property: functionScale <TBase extends Constructor>(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; } }; }
设置好这些之后,您可以创建一个类,该类表示应用 mixin 的基类:
// Compose a new class from the Sprite class, with the Mixin Scale applier: const EightBitSprite =Scale (Sprite); const flappySprite =new EightBitSprite("Bird"); flappySprite.setScale(0.8); console.log(flappySprite.scale);
受约束的混入
在上面的表单中,mixin 没有类的基础知识,这会使您很难创建所需的设计。为了对此建模,我们修改原始构造函数类型以接受泛型参数。
// 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<T = {}> = new (...args: any[]) => T;
这允许创建仅使用受约束基类的类:
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
然后,您可以创建 mixin,只有当您有一个特定的基础来构建时,它才能工作:
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// This mixin will only work if it is passed a base
// class which has setPos defined because of the
// Positionable constraint.
this.setPos(0, 20);
}
};
}
备选模式
本文档的早期版本推荐了一种编写 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)
);
});
});
}
此模式较少依赖编译器,更多依赖代码库,以确保运行时和类型系统正确保持同步。
约束条件
通过代码流分析,在 TypeScript 编译器内部本机支持 mixin 模式。在一些情况下,您可以触及本机支持的边缘。
装饰器和混入
您不能使用修饰符通过代码流分析提供 mixin:/p>
// 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();
player.shouldFreeze;
Property 'shouldFreeze' does not exist on type 'Player'.
// 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>() {}
Spec.prop; // string
Spec.anotherProp; // string
对象混入
使用 es6 的Object.assign合并多个对象。
interface Name {
name: string
}
interface Age {
age: number
}
interface Sex {
sex: number
}
let people1: Name = { name: "张三" }
let people2: Age = { age: 20 }
let people3: Sex = { sex: 1 }
const people = Object.assign (people1,people2,people3)
//people 会被推断成一个交差类型 Name & Age & sex;
类的混入
首先声明两个 mixins 类(严格模式要关闭不然编译不过)。使用implements,把类当成了接口。我们可以这么做来达到目的,为将要 mixin 进来的属性方法创建出占位属性。这告诉编译器这些成员在运行时是可用的。
class A {
type: boolean = false;
changeType() {
this.type = !this.type
}
}
class B {
name: string = '张三';
getName(): string {
return this.name;
}
}
class C implements A,B{
type:boolean
changeType:()=>void;
name: string;
getName:()=> string
}
最后,创建这个帮助函数,帮我们做混入操作。它会遍历 mixins 上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。
Object.getOwnPropertyNames()可以获取对象自身的属性,除去他继承来的属性,对它所有的属性遍历,它是一个数组,遍历一下它所有的属性名。
Mixins(C, [A, B])
function Mixins(curCls: any, itemCls: any[]) {
itemCls.forEach(item => {
Object.getOwnPropertyNames (item.prototype).forEach(name => {
curCls.prototype[name] = item.prototype[name]
})
})
}
let c = new C()
