TypeScript 装饰器的使用方法和应用场景

一、装饰器简介

装饰器是一种特殊类型的声明,用于注释类、方法、访问器、属性或参数。装饰器使用 @expression 这种形式,其中 expression 必须求值为一个函数,该函数会在运行时被调用。

装饰器本质上是一个函数,它可以在类声明、方法、访问器、属性或参数上使用。装饰器的主要作用是扩展或修改被装饰的目标的行为。

二、类装饰器

类装饰器是一个函数,它接收一个类的构造函数作为参数,并可以返回一个新的类构造函数来替换原来的类。

// 定义一个类装饰器函数,接收类的构造函数作为参数
function Demo(target: Function) {
    
    
  console.log(target); // 输出被装饰的类构造函数
}

// 使用 @Demo 装饰器
@Demo
class Person {
    
     }

// 当 Person 类被定义时,Demo 函数会被立即执行

应用举例:重写 toString 方法并封闭原型对象。

// 定义一个类装饰器,用于重写 toString 方法并封闭原型对象
function CustomString(target: Function) {
    
    
  // 向类的原型上添加自定义的 toString 方法
  target.prototype.toString = function () {
    
    
    return JSON.stringify(this); // 返回对象的 JSON 字符串表示
  };

  // 封闭原型对象,禁止随意修改
  Object.seal(target.prototype);
}

// 使用 @CustomString 装饰器
@CustomString
class Person {
    
    
  constructor(public name: string, public age: number) {
    
     }
}

// 测试代码
const p1 = new Person('张三', 18);
console.log(p1.toString()); // 输出:{"name":"张三","age":18}

关于返回值:类装饰器可以返回一个新的类来替换原类。

// 定义一个类装饰器,返回一个新类
function demo(target: Function) {
    
    
  // 返回一个新类,替换原类
  return class {
    
    
    test() {
    
    
      console.log(200); // 新类的方法
    }
  };
}

// 使用 @demo 装饰器
@demo
class Person {
    
    
  test() {
    
    
    console.log(100); // 原类的方法
  }
}

// 测试代码
console.log(new Person().test()); // 输出:200
三、装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,允许为装饰器传递参数。

// 定义一个装饰器工厂,接收一个参数 n
function LogInfo(n: number) {
    
    
  // 返回一个类装饰器函数
  return function (target: Function) {
    
    
    // 修改类的原型,添加 introduce 方法
    target.prototype.introduce = function () {
    
    
      for (let i = 0; i < n; i++) {
    
    
        console.log(`我的名字:${
      
      this.name},我的年龄:${
      
      this.age}`); // 循环输出 n 次
      }
    };
  };
}

// 使用 @LogInfo 装饰器,传递参数 3
@LogInfo(3)
class Person {
    
    
  constructor(public name: string, public age: number) {
    
     }
}

// 测试代码
const p1 = new Person('张三', 18);
p1.introduce(); // 输出三次:我的名字:张三,我的年龄:18
四、装饰器组合

装饰器可以组合使用,执行顺序为:先由上到下执行装饰器工厂,再由下到上执行装饰器。

// 装饰器函数
function test1(target: Function) {
    
    
  console.log('test1');
}

// 装饰器工厂
function test2() {
    
    
  console.log('test2工厂');
  return function (target: Function) {
    
    
    console.log('test2');
  };
}

// 装饰器工厂
function test3() {
    
    
  console.log('test3工厂');
  return function (target: Function) {
    
    
    console.log('test3');
  };
}

// 装饰器函数
function test4(target: Function) {
    
    
  console.log('test4');
}

// 组合使用装饰器
@test1
@test2()
@test3()
@test4
class Person {
    
     }

// 输出顺序:
// test2工厂
// test3工厂
// test4
// test3
// test2
// test1
五、属性装饰器

属性装饰器用于装饰类的属性,可以监视属性的修改。

// 定义一个属性装饰器,用于监视属性的修改
function State(target: object, propertyKey: string) {
    
    
  // 存储属性的内部值
  const key = `__${
      
      propertyKey}`;

  // 使用 Object.defineProperty 替换类的原始属性
  Object.defineProperty(target, propertyKey, {
    
    
    get() {
    
    
      return this[key]; // 获取属性值
    },
    set(newVal: string) {
    
    
      console.log(`${
      
      propertyKey}的最新值为:${
      
      newVal}`); // 输出属性的新值
      this[key] = newVal; // 设置属性值
    },
    enumerable: true, // 允许枚举
    configurable: true, // 允许配置
  });
}

// 使用 @State 装饰器
class Person {
    
    
  @State age: number; // 被装饰的属性

  constructor(public name: string, age: number) {
    
    
    this.name = name;
    this.age = age; // 触发 setter 方法
  }
}

// 测试代码
const p1 = new Person('张三', 18);
p1.age = 20; // 输出:age的最新值为:20
六、方法装饰器

方法装饰器用于装饰类的方法,可以在方法执行前后添加额外逻辑。

// 定义一个方法装饰器,用于在方法执行前后添加日志
function Logger(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    
    
  // 保存原始方法
  const original = descriptor.value;

  // 替换原始方法
  descriptor.value = function (...args: any[]) {
    
    
    console.log(`${
      
      propertyKey}开始执行......`); // 执行前日志
    const result = original.call(this, ...args); // 调用原始方法
    console.log(`${
      
      propertyKey}执行完毕......`); // 执行后日志
    return result;
  };
}

// 使用 @Logger 装饰器
class Person {
    
    
  @Logger
  speak() {
    
    
    console.log(`你好,我的名字:${
      
      this.name},我的年龄:${
      
      this.age}`); // 原始方法逻辑
  }
}

// 测试代码
const p1 = new Person('张三', 18);
p1.speak();
七、访问器装饰器

访问器装饰器用于装饰类的访问器(getter 和 setter),可以限制设置的值范围。

// 定义一个访问器装饰器,用于限制设置的值范围
function RangeValidate(min: number, max: number) {
    
    
  return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    
    
    // 保存原始的 setter 方法
    const originalSetter = descriptor.set;

    // 替换 setter 方法,加入范围验证逻辑
    descriptor.set = function (value: number) {
    
    
      if (value < min || value > max) {
    
    
        throw new Error(`${
      
      propertyKey}的值应该在 ${
      
      min}${
      
      max}之间!`); // 验证失败抛出错误
      }
      if (originalSetter) {
    
    
        originalSetter.call(this, value); // 调用原始 setter 方法
      }
    };
  };
}

// 使用 @RangeValidate 装饰器
class Weather {
    
    
  private _temp: number;

  constructor(temp: number) {
    
    
    this._temp = temp;
  }

  @RangeValidate(-50, 50)
  set temp(value: number) {
    
    
    this._temp = value;
  }

  get temp() {
    
    
    return this._temp;
  }
}

// 测试代码
const w1 = new Weather(25);
w1.temp = 67; // 抛出错误:temp的值应该在 -50 到 50之间!
八、参数装饰器

参数装饰器用于装饰方法的参数,可以限制参数的类型。

// 定义一个参数装饰器,用于标记参数不能为数字
function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
    
    
  // 初始化或获取当前方法的参数索引列表
  let notNumberArr: number[] = target[`__notNumber_${
      
      propertyKey}`] || [];
  notNumberArr.push(parameterIndex); // 添加当前参数索引
  target[`__notNumber_${
      
      propertyKey}`] = notNumberArr; // 存储回目标对象
}

// 定义一个方法装饰器,用于验证参数类型
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    
    
  const method = descriptor.value;

  descriptor.value = function (...args: any[]) {
    
    
    // 获取被标记为不能为数字的参数索引列表
    const notNumberArr: number[] = target[`__notNumber_${
      
      propertyKey}`] || [];

    // 检查参数类型
    for (const index of notNumberArr) {
    
    
      if (typeof args[index] === 'number') {
    
    
        throw new Error(`方法 ${
      
      propertyKey} 中索引为 ${
      
      index} 的参数不能是数字!`); // 验证失败抛出错误
      }
    }

    // 调用原始方法
    return method.apply(this, args);
  };
}

// 使用 @Validate 和 @NotNumber 装饰器
class Student {
    
    
  name: string;

  constructor(name: string) {
    
    
    this.name = name;
  }

  @Validate
  speak(@NotNumber message1: any, message2: any) {
    
    
    console.log(`${
      
      this.name}想对说:${
      
      message1}${
      
      message2}`); // 原始方法逻辑
  }
}

// 测试代码
const s1 = new Student("张三");
s1.speak(100, 200); // 抛出错误:方法 speak 中索引为 0 的参数不能是数字!