装饰器详解「10 分钟了解 TS 元编程」

🚀 个人简介:某大型测绘遥感企业资深Webgis开发工程师,软件设计师(中级)、优快云优质创作者
💟 作 者:柳晓黑胡椒❣️
📝 专 栏:js基础
🌈 若有帮助,还请关注点赞收藏,不行的话我再努努力💪💪💪

什么是装饰器

装饰器是一种特殊的声明,可以附加在 类、方法、属性、参数 上。顾名思义,它就是用来“装饰”的主体为目标对象 添加额外行为 ✨。

👉 装饰器的基本形式是 @expression,其中 expression 必须返回一个函数,并且在运行时执行,被装饰的对象作为参数传入。

装饰器的分类与执行顺序

@log2 // 类装饰器
@log1 // 类装饰器
class Test {
  private _age: number = 0;
  @nameDecorator // 属性装饰器
  public name: string = "";
  @sayDecorator // 方法装饰器
  public say(
    @paramWhoDecorator who: string, // 参数装饰器
    @paramAnythingDecorator anything: string // 参数装饰器
  ) {
    console.log(`${who}${anything}`);
  }
  @ageDecorator //访问器装饰器
  get age() {
    return this._age + 1;
  }
  set age(v: number) {
    this._age = v;
  }
  @staticDecorator //方法装饰器
  static getSomething() {
    return "getSomething";
  }
}

⚡ 执行顺序规则:

  1. 参数装饰器 → 方法装饰器 → 访问器装饰器 → 属性装饰器(实例成员)
  2. 参数装饰器 → 方法装饰器 → 访问器装饰器 → 属性装饰器(静态成员)
  3. 参数装饰器(构造函数参数)
  4. 类装饰器最后执行

👉 举例:say 方法的执行顺序是:

@paramAnythingDecorator -> @paramWhoDecorator -> @sayDecorator -> @nameDecorator -> @log1

类装饰器

📖 函数说明

  • 我们可以看一下类装饰器在 typescript 中的定义:
declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;
  • 作用:接收类的构造函数作为参数,可以扩展/修改类的原型

💡 使用场景

  • 用来为类统一加上 日志、依赖注入、Mixin 混入功能。
  • 常用于 框架(NestJS、TypeORM) 的元数据标记。
function log(constructor: any) {
  // 重写类中的方法
  constructor.prototype.getName = () => {
    console.log("getNameOverride");
  }; // 类新增方法
  constructor.prototype.getAge = () => {
    console.log("getAge");
  }; // 类新增属性
  constructor.prototype.age = "age";
  constructor.prototype.name = "xx";
}
@log
class Test {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  getName() {
    console.log("getName");
  }
}
const test = new Test("name");
test.getName(); // 可以打印出 getNameOverride
test.getAge();
console.log(test.age); // 可以打印出 age
console.log(test.name); 
// 打印的是name,而不是xx,通过装饰器是装饰到类的原型上,所以此处test.__proto__.name打印的才是xx

✨ 扩展:Mixin 混入

class A {
  getName() {
    return "name";
  }
}
class B {
  getAge() {
    return 18;
  }
}
@mixin(A, B)
class C {}
function mixin(...args: any[]) {
  // 调用函数返回装饰器实际应用的函数
  return function (constructor: any) {
    for (let arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === "constructor") continue; // 跳过构造函数
        Object.defineProperty(
          constructor.prototype,
          key, // @ts-ignore
          Object.getOwnPropertyDescriptor(arg.prototype, key)
        );
      }
    }
  };
}
let c = new C(); // @ts-ignore
console.log(c.getName(), c.getAge()); // 1, 2

方法装饰器

📖 函数说明

  • 类型定义:
declare type MethodDecorator = <T>(
 target: Object,
 propertyKey: string | symbol,
 descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
  • 参数含义:
  1. target:普通方法时为类的原型;静态方法时为构造函数
  2. propertyKey:方法名
  3. descriptor:方法的属性描述符

💡 使用场景

  • 方法增强:日志打印、性能监控、异常捕获。
  • 修改方法属性(如 writable、enumerable)。
function getNameDescorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  // 1、通过descriptor修改当前方法的属性,不能被重写
  descriptor.writable = false;
}
class Test {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  @getNameDescorator
  getName() {
    return this.name;
  }
}
const test = new Test("name");
(test as any).getName = () => {
  // 修改此方法就会报错 return 'xx'
};

⚡ 简单实现一个异常捕获的逻辑,这样可以在多处使用async/await:

import axios from "axios";
function catchError(target: any, key: string, descriptor: PropertyDescriptor) {
  const fn = descriptor.value;
  descriptor.value = async function () {
    try {
      return await fn.apply(this, arguments);
    } catch (e) {
      console.log(e);
    }
  };
}
class Test {
  @catchError
  async getName() {
    const name: string = await axios.get("xx");
    return name;
  }
  @catchError
  async getAge() {
    const age: number = await axios.get("yy");
    return age;
  }
}
const test = new Test();
test.getName();
test.getAge();

访问器装饰器

📖 函数说明

  • 访问器装饰器作用于 getter/setter。
  • 与方法装饰器类似,但专门用来控制属性的读取与赋值。

💡 使用场景

  • 给属性加上 前缀/后缀
  • 对属性访问进行 权限校验 / 日志记录

注意:TS 不允许同时装饰一个成员的 get 和 set。

function prefix(target: any, key: string, descorator: PropertyDescriptor) {
  descorator.get = function () {
    // @ts-ignore
    return `prefix_${this._name}`;
  };
}
class Test {
  _name: string = "xx";
  @prefix
  get name() {
    return this._name;
  }
  set name(name: string) {
    this._name = name;
  }
}
const test = new Test();
console.log(test.name); // 输出: prefix_xx

属性装饰器

📖 函数说明

  • 类型定义:
declare type PropertyDecorator = (
  target: Object,
  propertyKey: string | symbol
) => void;
  • 作用:给类的属性添加标记或元数据。

💡 使用场景

  • 常用于 依赖注入
  • 给属性添加 验证规则默认配置
interface ValidateConf {
  [propName: string]: any;
}
const validateConf: ValidateConf = {}; // 存储校验信息
// @ts-ignore
@validator
class Person {
  @validate("string")
  name: any;
  @validate("number")
  age: any;
  constructor(name: any, age: any) {
    this.name = name;
    this.age = age;
  }
}
function validator(constructor: any) {
  return class extends constructor {
    constructor(...args: any[]) {
      super(...args); // 遍历所有的校验信息进行验证
      // @ts-ignore
      for (let [key, type] of Object.entries(validateConf)) {
        if (typeof this[key] !== type)
          throw new Error(`${key} must be ${type}`);
      }
    }
  };
}
function validate(type: string) {
  return function (target: any, name: string) {
    // 向全局对象中传入要校验的属性名及类型
    validateConf[name] = type;
  };
}
new Person("Niko", "18"); // throw new error: [age must be number]

⚡ 这个类被定义后,先执行了属性装饰器,将对应的属性装饰缓存到validateConf对象中,并在类初始话的时候,重写了当前类的构造函数

参数装饰器

📖 函数说明

  • 类型定义:
declare type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;
  • 参数:
  1. target:类的原型
  2. propertyKey:方法名
  3. parameterIndex:参数位置索引

💡 使用场景

  • 参数校验(如不能为空)
  • 依赖注入(NestJS 常用)
  • 日志打印(记录调用参数)
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  @validate
  greet(@required name: string) {
    return "Hello " + name + ", " + this.greeting;
  }
}
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  let existingRequiredParameters: number[] =
    Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    requiredMetadataKey,
    existingRequiredParameters,
    target,
    propertyKey
  );
}
function validate(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  let method = descriptor.value;
  descriptor.value = function () {
    let requiredParameters: number[] = Reflect.getOwnMetadata(
      requiredMetadataKey,
      target,
      propertyName
    );
    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if (
          parameterIndex >= arguments.length ||
          arguments[parameterIndex] === undefined
        ) {
          throw new Error("Missing required argument.");
        }
      }
    }
    return method.apply(this, arguments);
  };
}

属性值必填,类创建完毕会立马执行参数装饰器,参数装饰器将自身必填的信息存到对应的方法元数据上,由于改写了方法的 value,所以在执行的时候执行的是方法的装饰器的内容,从中判断该参数是否必填。

takeaways

装饰器是元编程的重要工具,可以在不改变原始类或方法逻辑的情况下,给其附加功能。
它广泛应用于依赖注入(NestJS)、ORM(TypeORM)、日志系统等框架。
掌握不同装饰器的执行顺序和应用场景,就能灵活实现很多优雅的功能增强。
装饰器原理比较简单,但是可以实现很多实用并方便的功能。如果对装饰器比较感兴趣何以参考vue-property-decorator以及nestjs

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳晓黑胡椒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值