当前位置: 首页 > news >正文

Typescript 第五章 类和接口(多态,混入,装饰器,模拟final,设计模式)

第五章 类和接口

类是组织和规划代码的方式,是封装的基本单位。 typescript类大量借用了C#的相关理论,支持可见性修饰符,属性初始化语句,多态,装饰器和接口。

不过,由于Typescript将类编译成常规的JavaScript类,所以我们也能使用一些JavaScript管用法,例如兼顾类型安全的混入(mixin)。

Typescript的某些类特性,例如属性初始化语句和装饰器,JavaScript类也支持,因此能生成运行时代码。其他特性(例如可见性修饰符,接口和泛型)是Typescript专属的特性,只存在于编译时,把应用编译成JavaScript后不生成任何代码。

本章学习Typescript类的用法。掌握类的使用方法和缘由。

5.1 类的继承

我们将制作一个国际象棋引擎。提供一个API供两个玩家交替走棋。 首先草拟类型:

 // 表示以此国际象棋游戏class Game { }​// 表示一个国际象棋棋子class Piece { }​// 一个棋子的一组坐标class Position { }​// 将棋子分类class King extends Piece {}class Queen extends Piece {}// 主教class Bishop extends Piece {}// 骑士class Knight extends Piece {}// 城堡class Rook extends Piece {}// 兵class Pawn extends Piece {​}

每个棋子都有颜色和当前位置。

下面为Piece类添加颜色和位置

 type Color = "Black" | "White";// 竖线type File = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H";// 横线type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;// 表示以此国际象棋游戏​class Game { }​// 一个棋子的一组坐标class Position {constructor(private file: File,private rank: Rank) { }}​// 表示一个国际象棋棋子class Piece {protected position: Positionconstructor(private readonly color: Color,file: File,rank: Rank) {this.position = new Position(file, rank)}}
  • 由于颜色,横线,和竖线相对较少,因此可以手动把可能得值使用类型字面量列举出来。这样做限定了类型的范围,进一步提高了安全性。
  • 构造方法中的private访问修饰符自动把参数赋值给this(this.file等),并把可见性设为私有,这意味着Piece实例中代码可以读取和写入,但是Piece实例之外的代码不可以。不同的Piece实例访问各自的私有成员;其他类的实例,即便是Piece的子类也不可以访问私有成员
  • 把实例变量position的可见性声明为proteed。与private类似,protected也罢属性赋值给this,但是这样的属性对Piece的实例和Piece子类的实例都可见。声明position时没有为其赋值,因此在Piece构造方法中要赋值。如果未在构造方法中赋值,Typescript将提醒我们。也就是说,我们声明的类型是T,但事实上是T|undefined,这是因为我们没有在属性初始化语句或构造方法中为其赋值。如此一来,我们要更新属性的签名,指明其值不一定是一个Position实例,还有可能是undefined。
  • new Piece接受三个参:color,file和rank。我们为color指定了两个修饰符:一个是private,把他赋值给this,并确保只能由Piece的实例访问,一个是readonly,指明在初始赋值后,这个属性只能读取,不能再赋其他值。

Typescript类中的属性和方法支持三个访问修饰符

  • public 任何地方都能访问
  • protected 当前类和子类的实例能访问
  • private 当前类的实例访问

访问修饰符的作用是不让类暴露过多实现细节,而是只开放规范的API,供外部使用。

我们定义了一个Piece类,但是并不希望用户直接实例化Piece,而是在此基础上扩展,定义Queen,Bishop等类,实例化这些子类。为此,我们可以做出限制,具体方式是使用abstract:

 abstract class Piece {protected position: Positionconstructor(private readonly color: Color,file: File,rank: Rank) {this.position = new Position(file, rank)}}

现在实例化Piece将报错

abstract关键字表示,我们不能直接初始化该类,但是并不阻止我们在类中定义方法:

 abstract class Piece {protected position: Positionconstructor(private readonly color: Color,file: File,rank: Rank) {this.position = new Position(file, rank)}moveTo(position:Position){this.position = position}abstract canMoveTo(position:Position):boolean}​​// 将棋子分类class King extends Piece {canMoveTo(position: Position): boolean {return true}}

现在,Piece类包含以下信息

  • 告诉子类,子类必须实现一个名为canMoveTo的方法,而且要兼容指定的签名。如果忘记实现将报错。注意:实现抽象类的时候也要实现抽象方法
  • Piece类为moveTo方法提供了默认实现(如果子类愿意,也可以覆盖默认实现)。我们没有为moveTo方式访问修饰符,因此默认为public,所以其他代码可读也可写。

下面更新King类的定义

 class Position {constructor(private file: File,private rank: Rank) { }distanceFrom(position:Position){return {rank:Math.abs(position.rank - this.rank),file:Math.abs(position.file.charCodeAt(0)-this.file.charCodeAt(0))}}}// 将棋子分类class King extends Piece {canMoveTo(position: Position): boolean {let distance = this.position.distanceFrom(position)return distance.rank<2 && distance.file < 2}}

开始新游戏时,我们想自动创建一个棋盘和一些棋子:

 ​class Game {private pieces = Game.makePieces()private static makePieces() {return [new King("White", "E", 1),new King("White", "E", 8),new Queen("White", "D", 1),new Queen("Black", "D", 8)]}}

游戏其他功能自行实现

总结一下:

  • 类使用class关键字声明。扩展类时使用extend关键字。
  • 类可以是具体的,也可以是抽象的(abstract)。抽象类可以有抽象方法和抽象属性。
  • 方法的可见性可以是private,protected和public(默认)。抽象类可以有抽象方法和抽象属性。
  • 类可以有实例属性,可见性也可以是private,protected或public(默认)。实例属性可以在构造方法的参数中声明,也可通过属性初始化语句声明。
  • 声明实例属性时可以使用readonly把属性标记为只读

5.2 super

与JavaScript一样,Typescript也支持super调用。如果子类覆盖父类中定义的方法,在子类中可以通过super调用父类中的同名方法。super有两种调用方式:

  • 方法调用super.take.
  • 构造方法调用。使用特殊形式super(),而且只能在构造方法中调用。如果子类有构造方法,在子类的构造方法中必须调用super(),把父子关系联系起来(如果你忘记了,Typescript会提醒你)
 // 将棋子分类class King extends Piece {constructor(color: Color,file: File,rank: Rank){super(color,file,rank)}canMoveTo(position: Position): boolean {let distance = this.position.distanceFrom(position)return distance.rank < 2 && distance.file < 2}}

5.3 以this为返回类型

this可以用作值,此外还能用作类型。对类来说,this类型还可以用于注解方法的返回类型。

例如:实现ES6中set数据结构的简化版

 class Set{has(value:string):boolean{return true}add(value:string):Set{return this}}

定义Set的子类型

 class Set{has(value:string):boolean{return true}add(value:string):this{return this}}class MutableSet extends Set {delete(value:number):boolean{return true}// 不用覆盖了// add(value:number)}

扩展其他类时,要把返回this的每个方法的签名覆盖掉,就显得比较麻烦。如果只是为了让类型检查器满意,这样做就失去了继承基类的意义。

如此一来,哦我们可以把MutableSet中覆盖的add方法省略,因为在Set中this指向一个Set实例,而在MutableSet中,this指向一个MutableSet实例

5.4 接口

类经常当做接口使用 与类型别名相似,接口是一种命名类型的方式,这样就不用再行内定义了。类型别名和接口算是同一种概念的两种句法(就像函数表达式和函数声明之间的关系)

 <!-- 寿司 -->type Sushi = {calories:numbersalty:booleantasty:boolean}// 重写为interface Sushi{calories:numbersalty:booleantasty:boolean}

在使用Sushi类型别名的地方都能使用Sushi接口。两个声明都定义结构,而且二者可以相互赋值(其实,二者完全一样)

把类型组合在一起时,更为有趣。

 type Food = {calories:numbertasty:boolean}type Sushi = Food&{salty:boolean}interface Cake extends Food{sweet:boolean}

接口不一定扩展其他接口,其实,接口可以扩展任何结构:对象类型,类或其他接口。

类型和接口之间有什么区别呢?有三个细微的差别。

  1. 类型别名更加通用,右边可以是任何类型,包括类型表达式(类型,外加&或|等类型运算符);而在接口中,右边必须为结构。例如,下面类型别名不能使用接口重写:
 type A = number;type B = A | string
  1. 扩展接口时候,Typescript将检查扩展的接口是否可赋值给被扩展的接口:
 interface A {good(x:number):stringbad(x:number):string}interface B extends A {good(x:string|number):stringbad(x:string):string// 报错}

而使用别名没事

 type A = {good(x:number):stringbad(x:number):string}type B = A & {good(x:string|number):stringbad(x:string):string// 报错}let c:B = {good(x:number){return "s"},bad(x:string|number){return ""}}

建模对象类型的继承时,Typescript对接口所做的可赋值检查时捕获错误的有力工具

  1. 同一作用域中的多个同名接口将自动合并;同一作用域中的多个同名类型别名将导致编译错误。这个特性称为声明合并

5.4.1 声明合并

声明合并指的是Typescript自动把多个同名声明组合在一起。介绍枚举时讲过这个特性(3.2.12节),讨论命名空间声明(10.3节)还会说到

倘若生命了两个名为User的接口,Typescript将自动把二者组合成一个接口:

 interface User {name:string}​interface User {age:number}​let a:User = {name:"111",age:12}

而使用类型别名重写的话,将报错

注意:两个接口不能冲突,如果在一个接口中某个属性的类型为T,而在另一个接口中该属性的类型为U,由于T和U不是同一种类型,Typescript将报错

 interface User{age:string}interface User{age:number// 报错}​

如果接口中声明了泛型(5.7节),那么两个接口中要完全相同的方式声明泛型(名称一样还不行),这样才能合并接口。

 interface User<Age extends number>{age:Age}interface User<Age extends string>{age:Age}// 报错

Typescript很少会这么做,但是在这里,Typescript不仅检查了两个类型满不满足可赋值性,还会确认二者是否完全一致。

5.4.2 实现

声明类时,可以使用implement关键字指明该类满足某个接口。与其他显示类型注解一样,这是为类添加类型层面约束的一种便利方式。这么做能尽量保证类在实现上的准确性,防止错误出现在下游,不知具体原因。这也是实现常用的设计模式(例如适配器,工厂和策略)的一种常见方式。后后面分享

 interface Animal{eat(food:string):voidsleep(hour:string):void}class Cat implements Animal {eat(food: string): void {console.info("Ate some",food,".MM");}sleep(hour: string): void {console.info("Slept for",hour,"hours");}}new Cat().sleep("10")new Cat().eat("fish")​

cat必须实现Animal声明的每个方法。如果需要,在此基础上还可以实现其他方法和属性。

接口可以声明实例属性,但是不能带有可见性修饰符(private,protected,public),也不能使用static关键字。另外,像对象类型一样(第三章),可以使用readonly把实例属性标记为只读:

 interface Animal {readonly name: stringeat(food: string): voidsleep(hour: string): void}

一个类不限于只能实现一个接口,而是想实现多少个都可以:

 interface A {}interface B {}class C implements A,B{​}

5.4.3 实现接口还是扩展抽象类

实现接口其实于扩展抽象类差不多,区别是,接口更通用,更轻量,而抽象类的作用更具体,功能更丰富。

接口是对结构建模的方式。在值层面可以表示对象,数组,函数,类或类的实例。接口不生成JavaScript代码,只存在于编译时。

抽象类只能对类建模,而且生成运行时代码,即JavaScript类.抽象类可以有构造方法,可以提供默认实现,还能为属性和方法设置访问修饰符。这些在接口中都做不到。

具体使用哪个,取决于实现用途。如果多个类共用同一个实现,使用抽象类。如果需要一种轻量的方式表示“这个类是T型”,使用接口。

5.5 类是结构化类型

与Typescript中的其他类型一样,Typescript根据结构比较类,与类的名称无关。类与其他类型是否兼容,要看结构;如果常规的对象定义了同样的属性或方法,也与类兼容。从C#,Scala和其他多数名义类型编程语言转过来的程序员来说,一定要记住这点。 这意味着,如果一个函数接受Zebra实例,而我们传入一个Poodle实例,Typescript并不介意:

 class Zebra{// 小跑trot(){​}}// 贵宾犬class Poodle {trot(){​}}// 漫步function ambleAround(animal:Zebra){animal.trot()}let zebra = new Zebralet poodle = new Poodle​ambleAround(zebra)ambleAround(poodle)

Typescript是彻底的结构化类型语言,因此这段代码完全有效。

然后,如果类中使用private或proteceted修饰的字段,情况就不一样了。检查一个结构是否可赋值给一个类时,如果类中油private或protected字段,而且结构不是类或其子类的实例,那么结构就不可赋值给类:

 class A {private x = 1}class B extends A {}function f(a:A){}​f(new A)f(new B)f({x:1})// 错误​

5.6 类既声明值也声明类型

在Typescript中,多数时候,表达的要么是值要么时类型:

 let a = 1999function b(){​}// 类型type a = numberinterface b {():void}let c:b=function(){}​

在Typescript中,类型和值位于不同的命名空间中。根据场合,Typescript知道你要使用的是类型还是值(上面的a或b):

 if(a+1>3){}// 推导为值let x:a = 3// 推导为类型a

这种根据上下文进行解析的特性十分有用,可以做一些很酷的事情,例如实现伴生类型(companion type 6.3.4节)

类和枚举比较特殊,他们既在类型命名空间中生成类型,也在值命名空间中生成值。

 class C {}let c:C // 类型= new C// 值​enum E {F,G}let e:E// 类型= E.F// 值

使用类时,我们需要一种方式表达“这个变量应是这个类的实例”,枚举同样如此(“这个变量应是这个枚举的一个成员”)。由于类和枚举在类型层面生成类型,所以我们可以轻易表达这种”是什么“关系。

此外,我们还需要一种在运行时表示类的方式,这样才能使用new实例化类,在类上调用静态方法,做元编程,使用instanceof操作,因此类还需要生成值。

在上述示例中,C指C类的一个实例。那要怎么表示C类自身的类型呢?使用typeof关键字(Typescript提供的类型运算符,作用类似于JavaScript中值层面的typeof,不过操作的是类型)。

下面声明一个StringDatabase类,实现一个简单的数据库:

 type State = {// 索引签名[key: string]: string}​class StringDatabase {state:State = {}get(key:string):string|null{return key in this.state ? this.state[key] : null}set(key:string,value:string):void{this.state[key] = value}static from(state:State):StringDatabase{let db = new StringDatabasefor(let key in state){db.set(key,state[key])}return db}}let db = StringDatabase.from({name:"red",age:"16"})console.log(db.get("name"));

这个类声明生成的类型是什么呢?是实例类型StringDatabase:

 interface StringDatabase{state:Stateget(key:string):string|nullset(key:string,value:string):void}

以及 构造方法类typeof StringDatabase:

 interface StringDatabaseConstructor{new():StringDatabasefrom(state:State):StringDatabase}

即,StringDatabaseConstructor只有一个方法.from,使用new运算符操作这个构造方法得到一个StringDatabase实例。这两个接口组合在一起对类的构造方法和实例进行建模。

new()那一行称为构造方法签名,Typescript通过这种方式表示指定的类型可以使用new运算符实例化。鉴于Typescript采用的是结构化类型,这是描述类的最佳方式,即可以通过new运算符实例化的是类。

类声明不仅在值层面和类型层面生成相关内容,而且在类型层面生成两部分内容:一部分表示类的实例,另一部分表示类的构造方法(通过类型运算符typeof获取)

5.7 多态

与函数和类型一样,类和接口对泛型参数也有深层次支持,包括默认类型和限制。泛型的作用域可以放在整个类或接口中,也可放在特定的方法中:

 class MyMap<K,V>{1.constructor(initialKey:K,initialValue:V){2。}get(key:K):V{3.}set(key:K,value:V):void{​}merge<K1,V1>(map:MyMap<K1,V1>):MyMap<K|K1,V|V1>{4.//}static of<K,V>(k:K,v:V):MyMap<K,V>{5.​}}
  1. 声明类时绑定作用域与为整个类的泛型。K和V在MyMap的每个实例方法和实例属性都可用。
  2. 在构造方法中不能声明泛型,应该在类声明中声明泛型
  3. 在类内部,任何地方都能使用作用域为整个类的泛型
  4. 实例方法可以访问类一级的泛型,而且自己也可以声明泛型。.merge方法使用了类一节的泛型K和V,同时还自己声明了两个泛型:K1和K2
  5. 静态方法不能访问类的泛型,这就像在值层面不能访问类的实例变量一样。of不能访问1.中声明的K和V,不过该方法自己声明了泛型K和V

接口也可以绑定泛型

 interface MyMap<K,V>{get(key:K):Vset(key:K,value:V):void}

与函数一样,我们可以显式为泛型绑定具体类型,也可以让Typescript自动推导:

 let a = new MyMap<string,number>("k",1)// MyMap<string,number>let b = new MyMap("k",true)//MyMap<string,boolean>a.get("k")a.set("k",false)

5.8 混入

JavaScript和Typescript都没有trait或mixin关键字,不过自己实现起来也不难。这两个特性都用于模拟多重继承(一个类扩展两个以上的类),可做面向角色编程。

这是一种编程风格,在这种风格中,我们不表述“这是一个Shape”,而是描述事物的属性,表述”这个东西可以度量“或者”这个东西有四条边“;我们不再关心”是什么“关系,转而描述”能做什么“和”有什么“关系。

下面我们自己手动实现混入。

混入这种模式把行为和属性混合到类中。按照惯例,混入有以下特性:

  • 可以有状态(即实例属性)
  • 只能提供具体方法(与抽象方法相反)
  • 可以有构造方法,调用的顺序与混入类的顺序一致

Typescript没有内置混入的概念,不过我们可以自己手动轻易实现。下面我们设计一个调试Typescript类的库,以此为例进行说明。作用是输出关于类的一些信息。

 class User{//}User.debug() // 求值结果为'User({"id":3,"name":"Emma Gluzman"})'

通过这个标准的.debug接口,用户便可以调试任何类。下面开始实现。我们将通过一个混入实现这个接口,将其命名为withEZDbug。混入其实就是一个函数,只不过这个函数接受一个类构造方法,而且返回一个类构造方法。这个混入的声明如下:

 type ClassConstructor = new(...args:any[])=>{} // 1.function withEZDebug<C extends ClassConstructor>(Class:C){// 2.return class extends Class {//3.constructor(...args:any[]){// 4.super(...args) //5.}}}
  1. 先声明类型ClassConstructor,表示任意构造方法。由于Typescript完全才用结构化类型,因此使用new运算符操作的就是构造方法。我们不知道这个构造方法接受什么类型,所以指明他可以接受任意个任意类型的参数注意:Typescript要求不叫严格:构造方法类型的参数必须any[](不能是void,unknown[]等),这样才能扩展。
  2. 声明withEZDebug混入,只接受一个类型参数,C。C至少是类构造方法。使用extends子句表示这一要求。我们让Typescript推导withEZDebug的返回类型,结果是C与该匿名类的交集
  3. 由于混入是接受一个构造方法并返回一个构造方法的函数,所以这里返回一个匿名类构造方法
  4. 类构造方法至少要接受传入的类型接受的参数。但是注意:,由于我们事先不知道将传入什么类,所以要尽量放宽要求,允许传入任意个任意类型的参数,跟ClassConstructor一样。
  5. 最后,因为这个匿名类扩展自其他类,为了正确建立父子关系,别忘了调用Class的构造方法(super方法)。

与常规的JavaScript一样,如果构造方法中没有什么逻辑,可以省略4.5.在这个withEZDebug示例中,我们不打算在构造方法中放任何逻辑,因此可以省略那两行。

准备工作后,下面开始实现调试功能。调用.debug时,我们想输出类的构造方法名称和实例的值:

 type ClassConstructor = new(...args:any[])=>{} // 1.function withEZDebug<C extends ClassConstructor>(Class:C){// 2.return class extends Class {//3.constructor(...args:any[]){// 4.super(...args) //5.}debug(){let Name = Class.constructor.name;let value = this.getDebugValue()return Name+'('+JSON.stringify(value)+')'}}}

!!!???这里要调用.getDebugValue方法,可以我们怎么样确保类实现了这个方法呢?

答案是,不接受常规的类,而是使用泛型确保传给withEZDebug的类定义了.getDebugValue方法:

 type ClassConstructor<T> = new(...args:any[])=>{} // 1.function withEZDebug<C extends ClassConstructor<{getDebugValue():object //1.2}>>(Class:C){// 2.return class extends Class {//3.constructor(...args:any[]){// 4.super(...args) //5.}debug(){let Name = Class.constructor.name;let value = this.getDebugValue()return Name+'('+JSON.stringify(value)+')'}}}
  1. 1.为ClassConstructor添加了一个泛型参数。
  2. 1.2为ClassConstructor绑定一个结构类型,C,规定传给withEZDebug的构造方法至少定义了.getDebugValue方法

最终代码

 type ClassConstructor<T> = new(...args:any[])=>{}​function withEZDebug<C extends ClassConstructor<{getDebugValue():object}>>(Class:C){return class extends Class {getDebugValue: any;constructor(...args:any[]){super(...args)}debug(){let Name = Class.constructor.name;// 当前作用域中查找,没有就去父类中找let value = this.getDebugValue()return Name+'('+JSON.stringify(value)+')'}}}​class HardToDebugUser{constructor(private id:number,private firstName:string,private lastName:string){}getDebugValue(){console.log("running");return{id:this.id,name:this.firstName+" "+this.lastName}}}​let User = withEZDebug(HardToDebugUser)let user = new User(3,"red","润")console.log("debug中",user.debug());

我们可以把任意多个混入混合到类中,为类增添更丰富的行为,而且这一切在类型上都是安全的。混入有助于封装行为,是描述可重用行为的一种重要方式。

很多语言,比如Scala,PHP,Kotlin和Rust,实现了精简版混入,称为性状(trait)。性状与混入类似,但是没有构造方法,也不支持实例属性。 因此性状更容易使用,而且不会在多个性状访问性状与基类共用的状态时产生冲突。

5.9 装饰器

装饰器是Typescript的一个实验特性,为类,类方法,属性和方法参数的元编程提供简介的句法。其实,装饰器就是子啊装饰目标上调用函数的一种句法。

tsconfig.json添加"experimentalDecorators": true开启装饰器,这是一个实验特性,目前Typescript5.x版支持装饰器第三阶段了,本文暂时没涉及,学好本节后可以快速过渡。

使用装饰器

 @serializeableclass APIPayload{getValue():Payload{//}}

不使用装饰器

 let APIPayload = serializeable(class APIPayload{getValue():Payload{//.}})

对不同种类的装饰器,Typescript要求作用域中有那种装饰器指定名称的函数,而且该函数还要具有相应的签名(见下表5-1)

表5-1:不同种类装饰器函数要具有的类型签名

装饰目标具有的类型签名
(Constructor:{new(...any[])=>any})=>any
方法(classPrototype:{},methodName:string,descriptor:PropertyDescriptor)=>any
静态方法(Constructor:{new(...any[])=>any}),methodName:string,descriptor:PropertyDescriptor)=>any
方法的参数(classPrototype:{},paramName:string,index:number)=>void
静态方法的参数(Constructor:{new(...any[])=>any}),paramName:string,index:number)=>any
属性(classPrototype:{},propertyName:string)=>any
静态属性(Constructor:{new(...any[])=>any},propertyName:string)=>any
属性设值方法/读值方法(classPrototype:{},propertyName:string,descriptor:PropertyDescriptor)=>any
静态属性设置方法/读值方法(Constructor:{new(...any[])=>any},propertyName:string,descriptor:PropertyDescriptor)=>any

Typescript没有内置任何装饰器,如果你要使用,只能自己实现,或者从npm中安装。不同种类的装饰器(包括类装饰器,方法装饰器,属性装饰器和函数参数装饰器)都是常规函数,只不过要满足相应的特定签名。例如,前面使用的@serializable装饰器可以像下面这样实现:

 type ClassConstructor<T> = new(...args:any[])=>T//1.​function serializeable<T extends ClassConstructor<{getValue():Payload// 2.}>>(Constructor:T){// 3.return class extends Constructor{// 4.serialize(){return this.getValue()}}}​@serializeableclass Payload{name="redrun"serialize:anygetValue(): Payload {console.log(this);return this}}let p = new Payload()p.serialize()
  1. 在Typescript中,类的构造方法使用new()表示结构化类型。如果类的构造方法可被扩展(使用extends),Typescript要求参数的类型为可展开的any,即new(…any[])
  2. @serializable可以装饰任何实现.getValue方法,而且返回一个Payload的类的实例
  3. 类装饰器是一个接受单个参数(即目标类)的函数。如果装饰器函数返回一个类,在运行时这个类将替换被装饰的类,否则,要返回原类
  4. 为了装饰目标类,我们返回一个扩展原类的类,增加.serialize方法。

Typescript假定装饰器不改变装饰器目标的结构,意即不增加或删除方法和属性。Typescript在编译时检查返回的类是否可以复制给传入的类。

在Typescript的装饰器称为稳定特性前,不建议使用。

使用常规用法

 let DecroatedAPIPayload = serialized(APIPayload)let payload = new DecoratedAPIPayloadpayload.serialize() // string

更多装饰器信息,官方文档

5.10 模拟final类

final关键字的作用:某些语言使用这个关键字把类标记为不可拓展,或者把方法标记为不可覆盖。

Typescript的类和方法不支持final关键字,但是我们可以轻易模拟

可以使用私有的构造方法模拟final类:

 class MessageQueue{private constructor(private message:string[]){}}class BadQueue extends MessageQueue{}//报错new MessageQueue()// 报错

除了禁止扩展类以外,私有的构造方法还禁止直接实例化类。但是,我们喜欢final类能够实例化,禁止拓展就好,那么,怎样保留第一个限制,而避免第二个限制呢;

 class MessageQueue{private constructor(private message:string[]){}static create(message:string[]){return new MessageQueue(message)}}// class BadQueue extends MessageQueue{}//报错// new MessageQueue()​MessageQueue.create([])// 创建一个类

5.11 设计模式

下面动手实现一两个设计模式

5.11.1 工厂函数

工厂模式(factory pattern)是创建某种类型的对象的一种方式,这种方式把创建哪种具体对象留给创建该对象的工厂决定。

 type Shoe = {purpose:string}class BalletFlat implements Shoe {purpose = "dancing"}class Boot implements Shoe{purpose = "woodcutting"}​

这里使用type,此外也可以使用interface

下面创建工厂

 type Shoe = {purpose:string}class BalletFlat implements Shoe {purpose = "dancing"}class Boot implements Shoe{purpose = "woodcutting"}​let Shoe = {create(type:"balletFlat"|"boot"):Shoe{switch(type){case "balletFlat":return new BalletFlatcase "boot":return new Boot}}}

这个实例使用伴生对象模式(6.3.4节)声明类型Shoe和同名值Shoe,以此表明值提供了操作类型的方法。若想使用这个工厂,只需要调用.create:

 Shoe.ceate("boot")

5.11.2 建造者模式

建造者模式(builder pattern)把对象的建造方式与具体的实现方式区分开。如果你用过jquery,或者ES6的Map和Set等数据结构,对这种API风格不陌生。

 class RequestBuilder {private url:string|null = nullprivate method:'get'|'post'|null = nullprivate data:object|null = nullsetURL(url:string):this{this.url = urlreturn this}setMethod(method:'get'|'post'):this {this.method = methodreturn this}setData(data:object):this{this.data = datareturn this}send(){//}​}​new RequestBuilder().setURL("/user").setMethod('get').setData({firstName:'Anna'}).send()​

按顺序调用

 class RequestBuilder {protected data: object | null = nullprotected method: 'get' | 'post' | null = nullprotected url: string | null = null​setMethod(method: 'get' | 'post'): RequestBuilderWithMethod {return new RequestBuilderWithMethod().setMethod(method).setData(this.data)}setData(data: object | null): this {this.data = datareturn this}}​class RequestBuilderWithMethod extends RequestBuilder {setMethod(method: 'get' | 'post' | null): this {this.method = methodreturn this}setURL(url: string): RequestBuilderWithMethodAndURL {return new RequestBuilderWithMethodAndURL().setMethod(this.method).setURL(url).setData(this.data)}}​class RequestBuilderWithMethodAndURL extends RequestBuilderWithMethod {setURL(url: string): this {this.url = urlreturn this}send() {// ...}}​new RequestBuilder().setMethod('get').setData({}).setURL('foo.com').send()

相关文章:

Typescript 第五章 类和接口(多态,混入,装饰器,模拟final,设计模式)

第五章 类和接口 类是组织和规划代码的方式&#xff0c;是封装的基本单位。 typescript类大量借用了C#的相关理论&#xff0c;支持可见性修饰符&#xff0c;属性初始化语句&#xff0c;多态&#xff0c;装饰器和接口。 不过&#xff0c;由于Typescript将类编译成常规的JavaScri…...

IFNULL()COALESCE()

在 MySQL 中&#xff0c;IFNULL() 函数是可用的&#xff0c;但是请注意它不能直接用于聚合函数的结果。要在聚合函数结果可能为 NULL 的情况下返回特定值&#xff0c;应该使用 COALESCE() 函数而不是 IFNULL() 函数。 以下是代码示例&#xff1a; COALESCE(SUM(pc.CONTRACT_T…...

WPF实战学习笔记23-首页添加功能

首页添加功能 实现ITodoService、IMemoService接口&#xff0c;并在构造函数中初始化。新建ObservableCollection<ToDoDto>、 ObservableCollection<MemoDto>类型的属性&#xff0c;并将其绑定到UI中修改Addtodo、Addmemo函数&#xff0c;将添加功能添加 添加添加…...

OpenCV-Python常用函数汇总

OpenCV Python OpenCV简述显示窗口waitKey()&#xff1a;等待按键输入namedWindow()&#xff1a;创建窗口destroyWindow() &#xff1a;注销指定窗口destroyAllWindows() 注销全部窗口resizeWindow() 调整窗口尺寸 图像操作imread()&#xff1a;读取图像imwrite()&#xff1a;保…...

Vue-router多级路由

目录 直接通过案例的形式来演示多级路由的用法 文件结构 Banner.vue <template><div class"col-xs-offset-2 col-xs-8"><div class"page-header"><h2>Vue Router Demo</h2></div></div> </template><…...

前端学习--vue2--2--vue指令基础

写在前面&#xff1a; 前置内容 - vue配置 文章目录 插值表达式v-html条件渲染v-show和v-ifv-ifv-if的扩展标签复用组件 v-show v-on /事件v-bind /&#xff1a;属性v-modelv-for 循环元素v-slotv-prev-cloak vue指令只的是带有v-前缀的特殊标签属性 插值表达式 插值表达式{…...

【Python机器学习】实验03 logstic回归

文章目录 简单分类模型 - 逻辑回归1.1 准备数据1.2 定义假设函数Sigmoid 函数 1.3 定义代价函数1.4 定义梯度下降算法gradient descent(梯度下降) 1.5 绘制决策边界1.6 计算准确率1.7 试试用Sklearn来解决2.1 准备数据(试试第二个例子)2.2 假设函数与前h相同2.3 代价函数与前相…...

面试-杨辉三角python递归实现,二进制转换

杨辉三角 def yang_hui(x,y):xint(x)yint(y)assert x>y,列数不应该大于行数# x 表示行&#xff0c;y表示列if y1 or yx:return 1else:return yang_hui(x-1,y-1)yang_hui(x-1,y)xinput(输入第几行) yinput(输入第几列) resultyang_hui(int(x),int(y)) print(result) #inclu…...

SPEC CPU 2017 x86_64 Ubuntu 22.04 LTS LLVM 16.0.6 编译 intrate intspeed

源码编译llvm 下载源码 yeqiangyeqiang-MS-7B23:~/Downloads/src$ git clone --depth1 -b 7cbf1a2 https://github.com/llvm/llvm-project 正克隆到 llvm-project... warning: 不能发现要克隆的远程分支 7cbf1a2。 fatal: 远程分支 7cbf1a2 在上游 origin 未发现 yeqiangyeqi…...

java备忘录模式

在Java中&#xff0c;备忘录模式&#xff08;Memento Design Pattern&#xff09;用于捕获一个对象的内部状态并在该对象之外保存这个状态。备忘录模式允许在后续需要时将对象恢复到之前保存的状态&#xff0c;而不会暴露其内部结构。 备忘录模式包含以下主要角色&#xff1a;…...

iOS--runtime

什么是Runtime runtime是由C和C、汇编实现的一套API&#xff0c;为OC语言加入了面向对象、运行时的功能运行时&#xff08;runtime&#xff09;将数据类型的确定由编译时推迟到了运行时平时编写的OC代码&#xff0c;在程序运行过程中&#xff0c;最终会转换成runtime的C语言代…...

06. 管理Docker容器数据

目录 1、前言 2、Docker实现数据管理的方式 2.1、数据卷&#xff08;Data Volumes&#xff09; 2.2、数据卷容器&#xff08;Data Volume Containers&#xff09; 3、简单示例 3.1、数据卷示例 3.2、数据卷容器示例 1、前言 在生产环境中使用 Docker&#xff0c;一方面…...

计算机视觉常用数据集介绍

1 MINIST MINIST 数据集应该算是CV里面最早流行的数据了&#xff0c;相当于CV领域的Hello World。该数据包含70000张手写数字图像&#xff0c;其中60000张用于train&#xff0c; 10000张用于test&#xff0c; 并且都有相应的label。图像的尺寸比较小&#xff0c; 为28x28。 数…...

Arcgis画等高线

目录 数据准备绘制等高线3D等高线今天我们将学习如何在ArcGIS中绘制等高线地图。等高线地图是地理信息系统中常见的数据表现形式,它通过等高线将地形起伏展现得一目了然,不仅美观,还能提供重要的地形信息。 数据准备 在开始之前,确保已经准备好了高程数据,它通常以栅格数…...

abp vnext4.3版本托管到iis同时支持http和https协议

在项目上本来一直使用的是http协议,后来因为安全和一些其他原因需要上https协议&#xff0c;如果发布项目之后想同时兼容http和https协议需要更改一下配置信息&#xff0c;下面一起看一下&#xff1a; 1.安装服务器证书 首先你需要先申请一张服务器证书&#xff0c;申请后将证…...

2023年全网电视盒子无线ADB修改桌面(无需ROOT)

前言 1.主要是为了解决电视盒子等安卓设备无法卸载或者停用原始桌面导致无法选用第三方桌面。 解决方案 1.首先自行下载我提供的网盘APK 2.点击打开中国移动云盘 3.不管你是通过U盘还是局域网共享能够让你的电视安装第三方应用&#xff0c;毕竟每个品牌的安装方法不尽相同…...

什么是Java中的Maven?

Java中的Maven&#xff0c;可以简单理解为“一个神奇的工具”&#xff0c;它可以自动帮你管理Java项目的依赖关系&#xff0c;让你不再为手动下载、配置各种库而烦恼。想象一下&#xff0c;你正在写一个Java项目&#xff0c;突然发现需要引入一个名为"第三方库"的模块…...

【C++】总结7

文章目录 函数指针C中类成员的访问权限和继承权限问题定义和声明的区别C中类的静态成员与普通成员的区别是什么&#xff1f;虚函数为什么不能重载为内联函数&#xff1f;对ifdef endif的理解如何在不使用额外空间的情况下&#xff0c;交换两个数&#xff1f; 函数指针 什么是函…...

【前端知识】React 基础巩固(四十二)——React Hooks的介绍

React 基础巩固(四十二)——React Hooks的介绍 一、为什么需要Hook? Hook 是 React 16.8 的新增特性&#xff0c;它可以让我们在不编写class的情况下使用state以及其他的React特性&#xff08;比如生命周期&#xff09;。 class组件 VS 函数式组件&#xff1a; class的优势…...

adb命令丨adb push命令大全_adb操控手机和指令

【ADB命令】adb push命令总结 adb push命令大全操控手机和指令 运行在 Android 设备上的adb后台进程 执行 adb shell ps | grep adbd &#xff0c;可以找到该后台进程&#xff0c;windows 请使用 findstr 替代 grep [xuxu:~]$ adb shell ps | grep adbd root 23227 1 6672 8…...

Java多线程从入门到精通

一、基础概念 1.1 进程与线程 进程是指运行中的程序。 比如我们使用浏览器&#xff0c;需要启动这个程序&#xff0c;操作系统会给这个程序分配一定的资源&#xff08;占用内存资源&#xff09;。 线程是CPU调度的基本单位&#xff0c;每个线程执行的都是某一个进程的代码的某…...

MySQL技术内幕1:内容介绍+MySQL编译使用介绍

文章目录 1.整体内容介绍2.下载编译流程2.1 安装编译工具和依赖库2.2 下载编译 3.配置MySQL3.1 数据库初始化3.2 编辑配置文件3.3 启动停止MySQL3.4 登录并修改密码 1.整体内容介绍 MySQL技术系列文章将从MySQL下载编译&#xff0c;使用到MySQL各组件使用原理源码分析&#xf…...

设计模式-观察着模式

观察者模式 观察者模式 (Observer Pattern) 是一种行为型设计模式&#xff0c;它定义了对象之间一种一对多的依赖关系&#xff0c;当一个对象&#xff08;称为主题或可观察者&#xff09;的状态发生改变时&#xff0c;所有依赖于它的对象&#xff08;称为观察者&#xff09;都…...

【Unity】R3 CSharp 响应式编程 - 使用篇(二)

一、通用的事件监听用法 using System;using R3;using UnityEngine;namespace Aladdin.Standard.Observable.Common{public class CommonObservable : MonoBehaviour{// 默认会调用1次public SerializableReactiveProperty<int> serializableReactiveProperty;…...

如何通过外网访问内网服务器?怎么让互联网上连接本地局域网的网址

服务器作为一个数据终端&#xff0c;是很多企事业单位不可获缺的重要设备&#xff0c;多数公司本地都会有部署服务器供测试或部署一些网络项目使用。有人说服务器就是计算机&#xff0c;其实这种说法不是很准确。准确的说服务器算是计算机的一种&#xff0c;它的作用是管理计算…...

174页PPT家居制造业集团战略规划和运营管控规划方案

甲方集团需要制定一个清晰的集团价值定位&#xff0c;从“指引多元”、“塑造 能力”以及“强化协同”等方面引领甲方做大做强 集团需要通过管控模式、组织架构及职能、授权界面、关键流程、战略 实施和组织演进路径&#xff0c;平衡风险控制和迅速发展&#xff0c;保证战略落地…...

代码随想录算法训练营第60期第六十天打卡

大家好&#xff0c;今天因为有数学建模比赛的校赛&#xff0c;今天的文章可能会简单一点&#xff0c;望大家原谅&#xff0c;我们昨天主要讲的是并查集的题目&#xff0c;我们复习了并查集的功能&#xff0c;我们昨天的题目其实难度不小&#xff0c;尤其是后面的有向图&#xf…...

面壁智能推出 MiniCPM 4.0 端侧大模型,引领端侧智能新变革

在 2025 智源大会期间&#xff0c;面壁智能重磅发布了开源模型 MiniCPM 4.0 的两个新版本&#xff08;0.5B、8B&#xff09;&#xff0c;代号「前进四」。此次发布在人工智能领域引发了广泛关注&#xff0c;标志着端侧大模型技术取得了重大突破。 卓越性能&#xff0c;树立行业…...

华为手机开机卡在Huawei界面不动怎么办?

遇到华为手机卡在启动界面&#xff08;如HUAWEI Logo界面&#xff09;的情况&#xff0c;可依次尝试以下解决方案&#xff0c;按操作复杂度和风险由低到高排序&#xff1a; &#x1f527; 一、强制重启&#xff08;优先尝试&#xff09; 1.通用方法‌ 长按 ‌电源键 音量下键‌…...

前端面试五之vue2基础

1.属性绑定v-bind&#xff08;&#xff1a;&#xff09; v-bind 是 Vue 2 中用于动态绑定属性的核心指令&#xff0c;它支持多种语法和用法&#xff0c;能够灵活地绑定 DOM 属性、组件 prop&#xff0c;甚至动态属性名。通过 v-bind&#xff0c;可以实现数据与视图之间的高效同…...