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: Position
     constructor(
         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: Position
     constructor(
         private readonly color: Color,
         file: File,
         rank: Rank
     ) {
         this.position = new Position(file, rank)
     }
 }

现在实例化Piece将报错

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

 abstract class Piece {
     protected position: Position
     constructor(
         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:number
     salty:boolean
     tasty:boolean
 }
 // 重写为
 interface Sushi{
     calories:number
     salty:boolean
     tasty:boolean
 }

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

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

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

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

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

  1. 类型别名更加通用,右边可以是任何类型,包括类型表达式(类型,外加&或|等类型运算符);而在接口中,右边必须为结构。例如,下面类型别名不能使用接口重写:

    type A = number;
    type B = A | string

  2. 扩展接口时候,Typescript将检查扩展的接口是否可赋值给被扩展的接口:

    interface A {
    good(x:number):string
    bad(x:number):string
    }
    interface B extends A {
    good(x:string|number):string
    bad(x:string):string// 报错
    }

而使用别名没事

 type A = {
     good(x:number):string
     bad(x:number):string
 }
 type B = A & {
     good(x:string|number):string
     bad(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):void
     sleep(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: string
     eat(food: string): void
     sleep(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 Zebra
 let 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 = 1999
 function b(){
 ​
 }
 // 类型
 type a = number
 interface 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 StringDatabase
         for(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:State
     get(key:string):string|null
     set(key:string,value:string):void
 }

以及 构造方法类typeof StringDatabase:

 interface StringDatabaseConstructor{
     new():StringDatabase
     from(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):V
     set(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)+')'
         }
     }
 }

答案是,不接受常规的类,而是使用泛型确保传给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版支持装饰器第三阶段了,本文暂时没涉及,学好本节后可以快速过渡。

使用装饰器

 @serializeable
 class 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()
         }
     }
 }
 ​
 @serializeable
 class Payload{
     name="redrun"
     serialize:any
     getValue(): 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 DecoratedAPIPayload
 payload.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 BalletFlat
             case "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 = null
     private method:'get'|'post'|null = null
     private data:object|null = null
     setURL(url:string):this{
         this.url = url
         return this
     }
     setMethod(method:'get'|'post'):this {
         this.method = method
         return this
     }
     setData(data:object):this{
         this.data = data
         return this
     }
     send(){
         //
     }
 ​
 }
 ​
 new RequestBuilder()
 .setURL("/user")
 .setMethod('get')
 .setData({firstName:'Anna'})
 .send()
 ​

按顺序调用

 class RequestBuilder {
   protected data: object | null = null
   protected method: 'get' | 'post' | null = null
   protected url: string | null = null
 ​
   setMethod(method: 'get' | 'post'): RequestBuilderWithMethod {
     return new RequestBuilderWithMethod().setMethod(method).setData(this.data)
   }
   setData(data: object | null): this {
     this.data = data
     return this
   }
 }
 ​
 class RequestBuilderWithMethod extends RequestBuilder {
   setMethod(method: 'get' | 'post' | null): this {
     this.method = method
     return 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 = url
     return this
   }
   send() {
     // ...
   }
 }
 ​
 new RequestBuilder()
   .setMethod('get')
   .setData({})
   .setURL('foo.com')
   .send()
相关推荐
匹马夕阳12 分钟前
async/await 处理异步操作详解
前端
轻口味24 分钟前
【每日学点鸿蒙知识】Web嵌套滚动体验、拷贝传递 ArrayBuffer异常问题、ObjectLink 的属性传递、构建读取参数
前端·华为·harmonyos
lauo24 分钟前
【智体OS】官方上新发布“空钥登陆”--方便访客使用智体操作系统OS和智体应用
前端·javascript·分布式·机器人·开源
哥谭居民000127 分钟前
普通的树形数据primevue的treetable组件的treetable[ ]
前端·javascript·算法
徐小黑ACG32 分钟前
JavaScript 基础
开发语言·javascript
龙少954332 分钟前
【前端实现pdf导出】
前端·pdf
轻口味33 分钟前
【每日学点鸿蒙知识】异步介绍、上传app报错、Web控件接口、应用名称自定义配置、ActionSheetOptions自定义
前端·华为·harmonyos
m0_748247801 小时前
构建 Java Web 应用程序:从 Servlet 到数据库交互(Eclipse使用JDBC连接Mysql数据库)
java·前端·数据库
一枚前端小姐姐1 小时前
Node多版本共存与切换--通过nvm切换
前端·node.js
郑大乾6662 小时前
vuex - 第一天
javascript·vue.js·node.js