混入类型(Mixins) - TypeScript 类的类型
混入类型(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: 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; } }; }
设置好这些之后,您可以创建一个类,该类表示应用 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 = new (...args: any[]) => T;
这允许创建仅使用受约束基类的类:
type Positionable = GConstructor void }>; type Spritable = GConstructor; type Loggable = GConstructor void }>;
然后,您可以创建 mixin,只有当您有一个特定的基础来构建时,它才能工作:
function Jumpable(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() { class Base { static prop: T; } return Base; } function derived() { class Derived extends base() { static anotherProp: T; } return Derived; } class Spec extends derived() {} 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()
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!