Typescript中的装饰器

0 阅读3分钟

TypeScript 装饰器(Decorator)详解

装饰器看起来很"魔法",但它的本质只有一句话:装饰器就是一个普通的函数@xxx 只是调用它的一种语法糖。


一、@ 到底把代码变成了什么

@Logger 写在类上方,等价于把类当参数传给 Logger 函数,并用函数返回值替换原类:

装饰器语法糖本质.png

二、例子 1:类装饰器(最简单)

// 装饰器就是一个函数,它接收"被装饰的类"作为参数
function Logger(target: Function) {
  console.log(`定义了类:${target.name}`);
}

@Logger
class User {
  name = 'Tom';
}

// 只要代码加载,就打印:定义了类:User
// 你不需要 new User(),装饰器在类定义时就已经执行了

三、例子 2:方法装饰器(最能说明问题)

关键在于通过改写 descriptor.value,把原方法"包"了起来:

function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;        // ① 先存下原始方法

  descriptor.value = function (...args: any[]) {   // ② 替换成一个新的函数
    console.log(`调用 ${key},参数:`, args);
    const result = original.apply(this, args);      // ③ 内部再调用原始方法
    console.log(`返回:`, result);
    return result;
  };
}

class Calculator {
  @Log
  add(a: number, b: number) {
    return a + b;
  }
}

new Calculator().add(1, 2);
// 输出:
// 调用 add,参数: [1, 2]
// 返回: 3

调用链如下:

① calc.add(1, 2)                 外部代码发起调用
        ↓
② 进入 wrapper 函数              打印日志、计时、鉴权…
        ↓
③ original.apply(this, args)     真正执行原始的 add
        ↓
④ 返回 3                         结果原样交回调用方

方法装饰器的调用链.png

四、例子 3:带参数的装饰器(装饰器工厂)

想让装饰器接受参数(比如 @Timeout(500)),就在外面再套一层函数:

function Timeout(ms: number) {                    // 外层:接收参数
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;             // 内层:真正的装饰器
    descriptor.value = function (...args: any[]) {
      const start = Date.now();
      const result = original.apply(this, args);
      console.log(`${key} 耗时 ${Date.now() - start}ms`);
      return result;
    };
  };
}

class Api {
  @Timeout(500)      // 注意加括号:Timeout(500) 返回的才是装饰器
  fetchData() {}
}

五、五种装饰器,看参数就能区分

装饰器写在哪儿,TypeScript 传给它的参数就不同,这是最快的识别方法:

写在哪参数签名用途举例
类上(target)给类加静态属性、混入方法
属性上(target, key)序列化、校验、依赖注入
方法上(target, key, descriptor)日志、缓存、防抖、权限
访问器 get/set(target, key, descriptor)读写拦截、数据转换
参数上(target, key, index)参数校验、依赖注入标记

六、多个装饰器时,谁先执行?

求值从上往下,应用从下往上——最靠下的装饰器反而最贴"内核":

@A
@B
@C
method() {}
// 等价于:method = A( B( C( method ) ) )
// 调用 method 时,A 最先跑,C 最后跑

包装结构(洋葱模型):

多装饰器的洋葱包装顺序.png

同一个类里不同位置的执行顺序,记一个大致规律:先成员、后类;成员里先参数、再方法/属性

@ClassDecorator              // 最后执行
class Demo {
  @Prop() name: string;      // 先于方法

  @Method()                  // 参数装饰器比方法装饰器更早
  run(@Param() id: number) {}
}

七、它到底有什么用?

装饰器真正解决的是"横切关注点"——那些跟业务逻辑无关、但到处都要写的代码:

class UserService {
  @Log                    // 日志
  @Cache(60)              // 缓存 60 秒
  @Validate               // 参数校验
  getUser(id: number) {
    return this.db.find(id);   // 业务逻辑保持干净
  }
}

主流框架的应用:

  • NestJS@Controller('/users')@Get(':id') 定义路由
  • TypeORM@Entity()@Column() 定义数据库映射
  • Angular@Component()@Injectable() 做依赖注入
  • Vue/React 生态@Prop()@Watch()

八、两个必须知道的坑

  1. 类声明不会被提升。装饰器在类定义的那一刻就执行,所以装饰器函数本身必须在该文件加载前就已定义。

  2. tsconfig 要开开关。旧式装饰器需要在 tsconfig.json 里加:

{ "compilerOptions": { "experimentalDecorators": true } }

TypeScript 5.0+ 支持了一套新的标准装饰器(去掉 experimentalDecorators 的旧语义,即 stage-3 装饰器),写法更规范但生态支持暂不完全。使用 NestJS、Angular 等框架时,仍需旧式的 experimentalDecorators: true


一句话总结

装饰器就是"接收目标 → 返回增强版目标"的函数@ 帮你把"传入"这一步省了。理解了这一点,框架里所有花哨的 @xxx 都不再神秘。