📔学习笔记 | 一篇文章让我牢记TypeScript装饰器的用法

1,288 阅读8分钟

小陈这几天在学习 TypeScript,发现装饰器这东西挺有意思,形式上有点像Java中的注解(虽然不写Java,但还是见过的)。俗话说的好,“好记性不如烂笔头”,想着用文章的形式记录下来,也作为自己对装饰器的一篇学习笔记吧。

什么是装饰器

TypeScript 中的装饰器本质上就是一个函数,顾名思义,它是对类进行装饰的工具,可以根据用户自定义扩展类的功能。

我觉得网上这个说法更言简意赅:装饰器通过声明性语法增加了在定义类时扩充类及其成员的能力。

根据装饰器修饰类型的不同,可分为以下四种类型:

  • 类装饰器 ClassDecorator
  • 方法装饰器 MethodDecorator
  • 属性装饰器 PropertyDecorator
  • 参数装饰器 ParameterDecorator

使用写法:@装饰器名称

环境搭建

// 全局安装TypeScript
npm i typescript -g

// 创建decorator.ts文件 => 初始化tsconfig.json
tsc --init

// 启动ts编译器并监听 => 随后会编译成 decorator.js 文件 => 执行decorator.js
tsc -w
node decorator.js

初次编写并调用装饰器的时候会出现编译错误,如下所示。 image.png

这个时候打开 tsconfig.json 文件,将下面两行注释取消即可。

image.png

环境搭建好之后,开启我们的装饰器篇章吧!

类装饰器⭐️

类装饰器只有一个参数,即被修饰的类的构造函数。

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

它在类被创建的时候立即执行,仅对类做修饰,而不对实例。

基本使用

// 定义一个类装饰器,参数为类的构造函数
const AddressDecorator: ClassDecorator = (target: Function) => {
    console.log(target);
    target.prototype.age = 18
    target.prototype.getAddress = () => {
        return '你心里'
    }
}

@AddressDecorator
class User {
    public sayHello(): void {
        console.log('Hello World');
    }
    // public getAddress(){} * 
}

// console.log(new User().getAddress()); *
console.log((new User() as any).getAddress())
console.log((<any>new User()).age);

VSCode中打印结果如下所示。

image.png

我们去控制台打印一下展开看看。

image.png

可以发现,AddressDecorator 类装饰器的参数确实是 User 构造函数,且我们在类装饰器中往该构造函数的原型链上添加了 getAddress 方法和 age 属性。

image.png

需要注意的是,若像上面这张图这样调用 getAddress 这个方法的时候会出现编译错误。

解决方法:要么使用断言,要么在构造函数上声明一下这个方法(只需声明,无需实现)

装饰器工厂

普通的装饰器没有办法传参,而使用装饰器工厂,根据不同的传入值实现不同的装饰器,因此它的返回值就是一个装饰器,适用于各种类型的装饰器。

const AddressDecoratorFactory = (address = '默认地址'): ClassDecorator => {
    return (target: Function) => {
            target.prototype.getAddress = () => address
        }
}

@AddressDecoratorFactory('想去哪就去哪')
class Player{}

console.log((new Player() as any).getAddress());   // 想去哪就去哪

方法装饰器⭐️

方法装饰器有三个参数,分别是 targetpropertyKeydescriptor

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

target:一个对象。若为 静态方法,则为类的构造函数;若是 普通方法,则是类的原型对象。

propertyKey:被修饰的方法的名字。

descriptor:一个对象,被修饰方法的属性描述符。其结构如下所示。

{
  value: [Function: 方法名],
  writable: true,       // 可写
  enumerable: false,    // 可枚举
  configurable: true    // 可配置
}

基本使用

const UpdateDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    console.log(target,propertyKey,descriptor);
    // 重写原来的函数
    descriptor.value = () => {
        console.log('new method!');
    }
}

class User {
    @UpdateDecorator
    public getName(){
        console.log('my name is xiaochen');
    }

    @UpdateDecorator
    static info(){
        console.log('this is a static method');
    }
}

new User().getName()
User.info()

image.png

通过打印顺序,我们可以发现装饰器在函数创建的时候立即执行,直接重写了被修饰的函数,而不是等到函数被调用的时候才执行。

装饰器工厂实现自定义console.log()

前面介绍了装饰器工厂,这里我们使用方法装饰器工厂来简单实现一个自定义的 console.log(),根据用户的设置打印不同的字体颜色及字体大小。

const MessageDecoratorFactory = (content: string, fontSize = 16, color = 'black'): MethodDecorator => {
    return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
        descriptor.value = () => {
            console.log(`%c${content}`,`font-size:${fontSize}px;color:${color}`)
        }
    }
}

class User {
    @MessageDecoratorFactory('登录成功', 20, 'green')
    success() {}

    @MessageDecoratorFactory('登录失败', 14, 'red')
    fail() {}
}

new User().success()
new User().fail()

image.png

装饰器工厂模拟网络请求

const RequestDecoratorFactory = (url: string): MethodDecorator => {
    return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
        // 先将原来的方法保存下来,之后再调用
        let method = descriptor.value
        // 模拟请求的过程与耗时
        new Promise(resolve => {
            setTimeout(() => {
                resolve({status: 200, name: '小陈同学吗'})
            }, 2000);
        }).then(res => {
            method(res)
        })
    }
}

// 定义UserInterface接口
interface UserInterface {
    status: number
    name: string
    [props: string]: any
}

class User {
    @RequestDecoratorFactory('https://www.xiaochentongxuema.com')
    public getUser(user: UserInterface) {
        console.log(user);
    }
}

模拟请求.gif

我们只需要填好网络请求的url,则可得到返回的数据。

属性装饰器⭐️

属性装饰器有两个参数,targetpropertyKey

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

target:一个对象。同方法装饰器,若为 静态属性,则为该类构造函数;若为 普通属性,则为该类的原型对象。

propertyKey:被修饰的属性名。

基本使用

这里用属性装饰器实现一下大小写转换。

const PropDecorator: PropertyDecorator = (target: Object, propertyKey: string | symbol) => {
    // console.log(target, propertyKey);
    let v: string
    // 这里其实是一个闭包,当设置user.name的时候,v被赋值,但并没有被消灭;当获取user.name的时候,转换为大写
    Object.defineProperty(target,propertyKey,{
        get: () => v.toUpperCase(),
        set: (value) => v = value
    })
}

class User {
    @PropDecorator
    public name: string | undefined

    @PropDecorator
    static _age: number
}

let user = new User()
user.name = 'ABcdEFgHijk'
console.log(user.name);  // ABCDEFGHIJK

参数装饰器⭐️

参数装饰器有三个参数,分别是 targetpropertyKeyparameterIndex

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

target:一个对象。按照该形参所在的方法类型 分为类的构造函数和类的原型对象。

propertyKey:方法名。【不是形参名】

parameterIndex:被修饰参数所在形参列表的索引。

基本使用

const ParamsDecorator: ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
    console.log(target, propertyKey, parameterIndex);
}

class User {
    info(name: string, @ParamsDecorator age: number){
        console.log(age);
    }
}

打印结果为

image.png

Reflect.metadata 元数据

是什么?

Reflect.metadata主要是用于设置/读取元数据的方法,而元数据值得是描述东西时用的数据。它可以帮助开发者在类或其成员上 挂载 一些元数据,(打印出来是看不到的),并且方便地进行数据的反射和获取。

reflect-metadata 项目地址

至于适用场景我也不太清楚,如果有了解的朋友欢迎评论区交流一下。

image.png

怎么用?

这里我只简单介绍两个API,分别是定义元数据和获取元数据。有更多需要的同学可以戳戳上面的链接。

  • Reflect.defineMetadata(metadataKey, metadataValue, target[, propertyKey])

    • metadataKey:元数据名称
    • metadataValue:元数据值
    • target:挂载元数据的对象
    • propertyKey:挂在元数据对象中的属性【可省略】
  • Reflect.getMetadata(metadataKey,target[, propertyKey])

    • metadataKey:元数据名称
    • target:挂载元数据的对象
    • propertyKey:挂在元数据对象中的属性【可省略】

除了这种写法,还有装饰器的形式。

class C {
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

结合装饰器实现参数验证

环境准备

// 下载reflect-metadta
npm i reflect-metadata -S

// 取消tsconfig.json里面的注释(在文章开头已经做过了)
"emitDecoratorMetadata": true,

具体实现

// 导入
import "reflect-metadata"

const RequiredDecorator: ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
    let requiredParams: number[] = []
    requiredParams.push(parameterIndex)
    // 定义元数据,将所需参数数组挂载到target对象中的propertyKey
    Reflect.defineMetadata('metadataKey',requiredParams,target,propertyKey)
}

const ValidateDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const method = descriptor.value
    // 这里不用箭头函数是因为箭头函数中没有arguments对象,巩固一下这个知识点。
    descriptor.value = function() {
        // 获取元数据,根据元数据名称
        const requiredParams: number[] = Reflect.getMetadata('metadataKey',target,propertyKey) || []
        requiredParams.forEach(index => {
            if(index > arguments.length || arguments[index] === undefined) {
                throw new Error('请传递有必要的参数')
            }
            // console.log(arguments);
            return method.apply(this,arguments)
        })
    }
}

class User {
    @ValidateDecorator
    public all(name: string, @RequiredDecorator id: number) {
        console.log(name,id);
    }
}

new User().all('小陈同学吗',1998)

上述代码是正常运行的,因为所需参数 id 我们给了。若不给呢,控制台则会抛出我们自定义的错误。

image.png

装饰器的执行顺序

装饰器是可以叠加使用的,那既然如此,装饰器的执行顺序是怎样的呢,我们来写一段测试代码看看。

const C1: ClassDecorator = () => console.log('类装饰器1');
const C2: ClassDecorator = () => console.log('类装饰器2');
const C3: ClassDecorator = () => console.log('类装饰器3');

const M1: MethodDecorator = () => console.log('方法装饰器1');
const M2: MethodDecorator = () => console.log('方法装饰器2');

const Prop1: PropertyDecorator = () => console.log('属性装饰器1');
const Prop2: PropertyDecorator = () => console.log('属性装饰器2');

const Param1: ParameterDecorator = () => console.log('参数装饰器1');
const Param2: ParameterDecorator = () => console.log('参数装饰器2');

@C1
@C2 @C3
class Thanks {
    @Prop1 @Prop2
    public id:number | undefined

    @M2
    public fun() {}

    @M1
    public sayHello(name: string, @Param1 @Param2 age: number) {
        console.log(name, age);
    }

    @Prop2
    public gender: string | undefined
}

打印结果如下。

image.png

我们可以发现,根据代码位置的不同,打印的结果也不同,但总体方向是从上往下的。

若装饰器叠加,执行顺序是 从下往上,从右往左

若不同类型装饰器相互作用,执行顺序具体情况具体分析,而不是像别的文章所说是一个固定的顺序。

  • 若参数装饰器和方法装饰器共同作用于同一方法,则 参数 => 方法, 这个是确定的。
  • 其余情况,根据代码书写顺序,从上往下(就像我们读代码的顺序,从上往下解析,不要和上面的记混了)。
  • 类装饰器总是在最后执行

结语

以上就是关于TypeScript装饰器的学习笔记,一开始觉得不多,写下来发现也花了不少时间。

如有纰漏,欢迎指出!