更实用的TypeScript指南:类和TS装饰器

2,172 阅读10分钟

本系列旨在帮你用正确的方式打开TypeScript。

前言

这篇内容主要包含了类和TS装饰器的相关用法和实践。

那么这里为什么要专门讲一下class呢?一方面,一些程序员会很喜欢面向对象的编程风格,class的使用也确实会带来很多好处;另一方面,今天的主角,TS中的装饰器是基于类的,而装饰器的使用,无疑可以为ts的开发带来很多便利。

很多成熟的ts框架中,都不乏装饰器的使用。而为了了解装饰器如何使用,我们就先来说说类。这有点“为了这点醋,我才包的这盘饺子”的味道了🤣。

系列文章

更实用的TypeScript指南:不墨迹的入门 - 掘金 (juejin.cn)

更实用的TypeScript指南:类和TS装饰器 - 掘金 (juejin.cn)

一个故事带你了解TypeScript程序类型 - 掘金 (juejin.cn)

类(class)

tips:如果对类已经有了了解,可以直接跳到装饰器部分

接下来,将会通过一个实例的完善,尽量简短快速的让你了解类(更多内容可以访问标题上的链接)

一个简单的类和使用

首先我们创建一个类,并用这个类来实现一个常用的功能(例如设置text)

class SetText {
    //类的成员
    text:string
    //类的构造方法
    constructor (str: string) {
        this.text = str
    }
    telltext(): void {
        console.log(this.text)
    }
}
let setText = new SetText('abc') //创建一个类的实例(其实是个对象)
setText.telltext() //telltext用来输出class里text成员
>> 'abc'

这里new一个实例的过程,简单来说就是新建了一个空对象,将类的成员绑定到对象的属性上,将类的方法绑定到对象的__proto__ 上。

类的继承和supper

接下来,我们让一个派生类去继承基类,并重写父类构造函数,实现text合并的方法

class JoinText extends SetText {
    textCom:string
    constructor(str1:string, str2:string) {
        super(str1) 
        //继承后的派生类的构造方法必须要有super调用
        this.textCom = str1 + str2
    }
    telltext():void {
        console.log(this.textCom)
    }
}
let setText = new JoinText('abc','efg')
console.log(setText)
>> JoinText { text: 'abc', textCom: 'abcefg' }
setText.telltext()
>> 'abcefg'

这里的super调用会执行基类的构造函数 ,目的就是为了让派生类继承基类的对象。所以super是必要的

私有属性

类的私有生成字段需要用#前缀来注明,这样实例化出来的对象无法直接访问到这个属性,只能通过getter,setter来取值和设置

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

getter,setter

我们创建两个特殊的‘方法’来获取或设定对象的一些值

class JoinText extends SetText {
    textCom:string
    constructor(str1:string, str2:string) {
        super(str1)
        this.textCom = str1 + str2
    }
    telltext():void {
        console.log(this.textCom)
    }
    get getStr1():string {
        return `${this.text}`
    }
    set setStr1(str2:string) {
        this.text = str2
    }
}
let setText = new JoinText('abc','efg')
setText.setStr1 = 'xxx'
console.log(setText.getStr1)
>> 'xxx'

虽然这里直接使用setText.text进行赋值效果是一样的,但目的是为了了解setter和getter的用法。重要的是,调用get,set时,并不是调用方法,而是作为属性来调用

static

最后,我们添加一个static成员,他是可以直接用类调用的,同时也可以被static方法调用。

class JoinText extends SetText {
    static from:string = 'SetText'  
    textCom:string
    constructor(str1:string, str2:string) {
        super(str1)
        this.textCom = str1 + str2
    }
    telltext():void {
        console.log(this.textCom)
    }
}
console.log(JoinText.from)
>> 'SetText'

至此,我们快速地了解了类的用法,接下来进入正题:装饰器的使用

装饰器

一句话概括:装饰器是用来在类的各个部分添加和设置各种东西的。

装饰器分为类装饰器,成员装饰器,方法装饰器,参数装饰器;分别放在一个类的前面,类的某个成员前面,方法的前面,方法的某个参数的前面。

我们先看一个实例(nestjs的一个控制器)(nest是一个以express为基础的typescript服务器框架)

@Controller('cats') //类装饰器
export class CatsController {
    @Get('/hello')
    @HttpCode(200)//方法装饰器
    recall(@Req()/*参数装饰器*/ request: Request): string {
        console.log(request) //将会输出请求的内容
        return 'hello nestjs'
    }
}

看不懂没关系,我们一步步的来解释每一句:

首先@Controller('cats'),它是一个类装饰器,要放在class的前面,他的作用是定义一个基本的控制器,即添加了一个路由。也就是说现在,这个类成为了一个控制器,具有了一个/cats的路由

@Get('/hello')@HttpCode(200)都是方法装饰器,它们放在类的方法前面,他们让这个类具有了更多的功能,分别是增加了一个/hello的子路由和一个返回200的状态码

@Req()是一个参数装饰器,他放在类中函数的参数前面,在这个实例中,他让这个参数具有了请求的参数的意义。

最终运行的结果是这样的:

屏幕截图 2021-10-03 110506.png

可以看到,在加这些装饰器之前,这个类其实仅有一个方法:

class CatsController {
    recall(request: Request):string {
        console.log(request)
        return 'hello nestjs'
    }
}

而这些装饰器,则给这个类增加了很多功能,从而达到了一个控制器( 处理传入的请求和向客户端返回响应)的作用。这就是装饰器在实际开发中的用法,通过高度的封装,更加快速的实现功能。同时也具备了OOP(面向对象开发)的特点。

基本使用

那么装饰器,是如何实现这些事情的呢?

首先,我们通过实现一个具有提示功能的类来了解类装饰器的用法

初始化:

class Toast{
    type:string
    tell(text:string):void {   
    }
}

类装饰器

现在这个类是什么功能都没有的。我们先添加一个类装饰器,使这个类具有一个tiltle

const Description:ClassDecorator = (target) => { //注1
    target.prototype.title = '我是一个Toast' //注2
}
​
@Description //装饰类Toast
class Toast{
    type:string
    tell(text:string):void {
        
    }
}
​
let toast = new Toast()
interface NewToast extends Toast{title:string}//注3console.log((toast as NewToast).title)
>> '我是一个Toast'
  • 注1:我们定义了一个Description装饰器,他的类型是ClassDecorator(类装饰器型)。注意,装饰器是一类函数不同的装饰器接收的参数不同,这里的类装饰器接收的参数target即为它装饰的类本身。我们在类装饰器中,通过给类本身的prototype上添加属性,从而达到操作类的目的。
  • 注2:target.prototype.title = '我是一个Toast'操纵原型,相当于在class中添加了一个title = '我是一个Toast'的成员,对原型不太了解的童鞋可以评论区评论,我会单独出一期关于原型的文章
  • 注3:这里我们定义了一个interface接口,并且继承了Toast和增添了title属性。为的就是通过断言让ts知道toast是有title的。不然会报错,因为这个类本来是没有title成员的。(另外也可以直接使用toast['title'],这样将不进行检查)

通过类装饰器,我们让一个类有了title。但是现在这个title是固定的,如果我们需要传进一个参数来设置title,这时就可以利用装饰器工厂来实现这件事情。

装饰器工厂

装饰器工厂说起来好像比较高大上,我个人理解就是在各种的装饰器外套上一个函数,以达到接收参数的作用🤣。

例如我们将上面的类装饰器改成一个装饰器工厂:

const Description = (title:string):ClassDecorator => { //注1
    return (target) => {
        target.prototype.title = title
    }
}
​
@Description('装饰器鸭装饰器📢')//注2
class Toast{
    type:string
    tell(text:string):void {
        
    }
}
​
let toast = new Toast()
interface NewToast extends Toast{title:string}
​
console.log((toast as NewToast).title)
>> '装饰器鸭装饰器📢'
  • 注1:装饰器工厂将类装饰器套在了一个函数中,并返回了它。所以这里函数的返回是一个类装饰器型。它像是一个普通函数一样使用,只不过返回了一个类装饰器。并没有那么神秘
  • 注2:由于装饰器工厂是一个函数调用后返回装饰器,所以这里我们在使用时就直接调用,并且写入参数。

通过类装饰器,我们完成了自定义类的title的功能

成员装饰器

接下来,我们添加一个成员装饰器,来装饰类的type,表示这个提示功能是个警告(warning)。

const initType:PropertyDecorator = (target, propertyKey) => { //注1
    target[propertyKey] = 'warning' //注2
}
​
@Description('装饰器鸭装饰器📢')
class Toast{
    @initType//注3
    type:string
    tell(text:string):void {
        
    }
}
​
let toast = new Toast()
console.log(toast.type)
>> 'warning'
  • 注1:成员装饰器同样是函数,它接收的参数分别为当前类(target)和它装饰的成员的名字(propertyKey)
  • 注2:这里的写法就相当于toast.type = warning
  • 注3:成员装饰器装饰谁哪个成员把谁放到哪个成员上面

这里自定义type也可以使用装饰器工厂,只不过返回的类型成了方法装饰器。写法大同小异,有兴趣的童鞋可以试一下。

方法装饰器

最后我们加上一个方法装饰器来装饰方法,让他打印出提示信息

const initTell:MethodDecorator = (target, propertyKey, descriptor) => { //注1
    (descriptor.value as unknown) = (text: string):void => { //注2
        console.log('warning!这里出了些问题:',text)
    }
    descriptor.writable = false
    // 这种写法ES5之后是没作用的
    // target[propertyKey] = (text: string):void => {
    //     console.log('warning!这里出了些问题:',text)
    // }
}
@Description('装饰器鸭装饰器📢')
class Toast{
    @initType
    type:string
    @initTell
    tell(text:string):void {    
    }
}
​
let toast = new Toast()
toast.tell('按钮')
>>'warning!这里出了些问题: 按钮'
  • 注1:方法装饰器的参数分别是:接收类原型对象,对于静态方法来说是类的构造函数(target);方法名;方法的属性描述符 ,这里的属性描述符我们经常用到 descriptor.valuedescriptor.writable
  • 注2:descriptor.value指向函数,我们可以利用它来重写函数
  • 注3:descriptor.writable = false时,实例就没办法用toast.tell = ...来改写函数了

参数装饰器

参数装饰器的样例如下:

//参数装饰器
const initText:ParameterDecorator = (target, key, index) => {
​
}
  • 注:掺入的参数分别是原型(target),方法名(key),和参数在参数集合中的位置(index,0开始)

它常常和其他的装饰器一同使用。

不看也无所谓的Tips:

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器,属性装饰器应用到每个实例成员。
  1. 参数装饰器,然后依次是方法装饰器,属性装饰器应用到每个静态成员。
  1. 参数装饰器应用到构造函数。
  1. 类装饰器应用到类。

可以看看的Tips:

相同级别装饰器是从上往下加载,但是是从下往上调用。即若有几个功能重复的同级别装饰器,最上面的装饰器功能会覆盖掉下面的装饰器

总结

通过对类和装饰器的统一概述,大体介绍了它的用法和作用。回过头看一下最开始给出的一个栗子,是不是思路清晰很多呢?之后,至少不应该在nestjs或者Angular这样的框架时,遇到装饰器不知道是怎么回事了叭🤣

另一方面,装饰器,类这种写法,其实是面向对象的编程特色。一些功能,使用原型链或者class都可以完成的事,以后可以尝试使用class来操作。熟悉面向对象的编程风格,不仅有助于开阔思维,听说还有助于找到对象哦🧐

对了,如果你喜欢这个系列,不妨关注我,一起学习,一起进步!