TS 中的装饰器总结

183 阅读3分钟

前言

TS 在平时项目中会用的很多,但是 装饰器 一直没有用到过,最近在学 nestjs,里面使用了大量的装饰器语法,今天来总结 TS装饰器

首先来看 装饰器 在 nestjs 中的使用

  • 例子1
@Module({
  controllers: [],
  providers: [],
})
export class CatsModule {}

@Injectable()
class CatsService{
    
}
  • 例子2
export class CatsService {
 constructor( @InjectModel(Cat.name) private catModel: Model<Cat> ) {}
}
  • 例子3
export class CatsController {

  @Get("/findAll")
  async findAll() {
    return this.catsService.findAll();
  }
  
  @Post("/findGt")
  async findGt(@Body("num") num: Number = 2) {
    return this.catsService.findGt(num);
  }
}

上面的 nest 代码分别用了类装饰器,参数装饰器,方法装饰器

使用装饰器可以让我们的代码变得简洁清晰, 今天我们来一探究竟

🔥 装饰器

一共有四种装饰器,分别是 类装饰器方法装饰器,属性装饰器,参数装饰器,它们都是由 @ 开头,紧跟一个函数,区别是函数参数不同

✈️ 类装饰器

我们定义类装饰器 classDecorator

@classDecorator
class Person {}

类的装饰器类型为 ts 的内置类型 ClassDecorator

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

我们来定义 classDecorator,参数只有一个,即当前实例
我们可以在他的原型上添加属性,所有的实例共用,定义 name 属性

const classDecorator:ClassDecorator = (target)=>{
  target.prototype.name = "tsk";
}

那么在所有的实例上都可以获取到 name 属性

const tsk = new Person()
tsk.name   // tsk

对于一些不需要动态属性的话,可以使用这种方法

很明显不是每个人的名字是不尽相同的,所以我们使用工厂函数创建,也可以说是高阶函数

工厂函数

传入参数 tsk

@classDecorator('tsk')
class Person {}

使用高阶函数,接受参数并返回 装饰器

const classDecorator = (name:string):ClassDecorator=>{
  return (target) => {
    target.prototype.name = name;
  }
}  

如果想要改变姓名,可以直接在装饰器中输入不同的参数即可

const tsk = new Person()
console.log(tsk.name) // tsk

🚢 方法装饰器

对类中的方法做装饰

class Person {
  @methodDecorator
  showName() {
    console.log("我是Tsk")
  }
}

装饰器类型为 ts 的内置类型 MethodDecorator

interface TypedPropertyDescriptor<T> {
    enumerable?: boolean;
    configurable?: boolean;
    writable?: boolean;
    value?: T;
    get?: () => T;
    set?: (value: T) => void;
}

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

同样的,我们定义 methodDecorator, 方法装饰器返回三个参数,分别是原型对象, 方法名 , 属性描述符

const methodDecorator: MethodDecorator = (target, key, descriptor) => {
  console.log(target, key, descriptor); 
};

image.png 同样的,由于 第一个参数是 原型对象,我们可以直接在上面加属性
比如:

const methodDecorator: MethodDecorator = (target, key, descriptor) => {
  target.sex = "男"
};

在实例对象上可以直接使用

const tsk = new Person()
console.log(tsk.sex) // 男

第三个参数是属性描述符,描述符的 value 属性 即 被装饰的函数体
我们可以直接调用

const methodDecorator: MethodDecorator = (target, key, descriptor) => {
  descriptor.value()  // 我是Tsk 
};

那么我们也可以修改函数体

const methodDecorator: MethodDecorator = (target, key, descriptor) => {
// 更改函数体
  descriptor.value = function(){
    console.log("我换名字了")
  }
};
// ......
const tsk = new Person()
tsk.showName() // 我换名字了

根据这个特性,我们可以做切面编程

const methodDecorator: MethodDecorator = (target, key, descriptor) => {
  let oldValue = descriptor.value;
  descriptor.value = function(){
    oldValue()
    console.log(`调用了方法${key}`)
  }
};
// ....
const tsk = new Person()
tsk.showName() // 我是Tsk,调用了方法showName

🚆 属性装饰器

依旧是我们熟悉的用法

class Person {
  @ageDecorator
  age: number;

  constructor(age: number) {
    this.age = age;
  }
}

装饰器类型为 ts 的内置类型 PropertyDecorator

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

返回两个参数,分别是原型对象和属性名

const ageDecorator:PropertyDecorator = (target, key) =>{
 console.log(target,key) // Person, 'age'
 target.sex = "男"
}

那么就在实例上可以使用

const person = new Person(20);
console.log(person.sex); // 男

在 nest 中的一个经典应用,使用 class-validator 校验类型

import {IsNumber} from "class-validator"
export class TodoDTO {
  @IsNumber({ message:"taskId 必须是一个数字" })
  taskId:number
}

如果类型不对,就会爆出taskId 必须是一个数字错误

我们来实现一下,首先IsNumber 是一个高阶函数,接受 一个对象,使用属性访问器 对 key 进行 定义

const IsNumber = ({message}:{message:string })=>{
  return (target: any, key: string)=> {
  let value = target[key];

  const getter = function () {
    return value;
  };

  const setter = function (newVal: number) {
    if (typeof newVal !== 'number' || isNaN(newVal)) {
      throw new Error(`${message}`);
    }
    value = newVal;
  };

  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

}  

class Person {
  @IsNumber({ message:"必须是一个数字" })
  age;

  constructor(age:any) {
    this.age = age;
  }
}

const person = new Person(20);
console.log(person.age); // Output: 20

person.age = 30; // Valid age value
console.log(person.age); // Output: 30

person.age = "Invalid"; // Invalid age value

image.png

🚗 参数装饰器

class Person {
  show(@parameterDecorator name:string){}
}

装饰器类型为 ts 的内置类型 ParameterDecorator

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

实现 装饰器,返回三个参数,原型对象方法名参数位置下标

const parameterDecorator:ParameterDecorator =(target,key,index)=>{
  console.log( target,key,index) // Peson,'show' ,0
}

❤️ 总结

TS 装饰器是一种强大的语言特性,可以让开发者在不改变原有代码结构的情况下扩展和修改类、方法或属性等元素的行为。它们可以用于实现诸如类型检查、依赖注入、日志记录、缓存等功能,使得代码更加模块化、可维护、易扩展。