理解TypeScript装饰器:它到底在做什么以及实现原理

前言

第一次接触装饰器时还是学Python的时候,当时就理解的不是很透彻。现在重新学TypeScript,又学到了装饰器,故记录一下目前的理解。

换个思路:先不看装饰器语法,而是想清楚它到底解决什么问题,然后一步步推导出它的实现方式。


装饰器

1. 没有装饰器前的痛点

假设有两个方法,都想知道谁被调用了、参数是什么、返回了什么:

class MathService {
  add(a: number, b: number): number {
    console.log("调用 add,参数:", a, b);
    const result = a + b;
    console.log("add 返回:", result);
    return result;
  }

  multiply(a: number, b: number): number {
    console.log("调用 multiply,参数:", a, b);
    const result = a * b;
    console.log("multiply 返回:", result);
    return result;
  }
}

问题很明显:每个方法里都塞了一堆和业务无关的日志代码。方法越多,重复越多。哪天想把 console.log 换成写文件,每个方法都得改。

我们希望写成这样:

class MathService {
  @log
  add(a: number, b: number): number {
    return a + b;
  }

  @log
  multiply(a: number, b: number): number {
    return a * b;
  }
}

方法里只写业务,日志交给 @log。问题是:@log 凭什么能插手add方法?

2. 关键前提:JavaScript里的方法可以被替换

先看一个基础事实。类的方法存在原型上:

class MathService {
  add(a, b) {
    return a + b;
  }
}

add本质上就是MathService.prototype上的一个属性,值是一个函数。既然是属性,就可以读取,也可以重新赋值:

const original = MathService.prototype.add;

MathService.prototype.add = function (a, b) {
  console.log("调用前");
  const result = original.call(this, a, b);
  console.log("调用后");
  return result;
};

这样每次调用math.add(1, 2),实际执行的就是新函数。装饰器做的就是这个事,只是写法更优雅,由TypeScript自动完成

3 装饰器就是一个函数
@log 里的 log,就是一个普通函数:

function log(target, propertyKey, descriptor) {
  // ...
}

当使用装饰器时:

class MathService {
  @log
  add(a, b) { return a + b; }
}

TypeScript在编译时,会自动把@log转换成类似这样的调用:

log(
  MathService.prototype,
  "add",
  Object.getOwnPropertyDescriptor(MathService.prototype, "add")
);

4. 三个参数从哪来

TypeScript 调用 log 时,按固定顺序传三个东西:

参数 含义 这里是什么
target 方法挂在谁身上 MathService.prototype
propertyKey 方法名 "add"
descriptor 方法的描述符 包含 valuewritable

descriptor 长这样:

{
  value: function (a, b) { return a + b; },
  writable: true,
  enumerable: false,
  configurable: true
}

5. 装饰器里到底改了什么

function log(target, propertyKey, descriptor) {
  var originalMethod = descriptor.value;   // ① 保存原方法

  descriptor.value = function (...args) {  // ② 把 descriptor.value 换成新函数
    console.log("调用方法: " + propertyKey + ",参数: " + JSON.stringify(args));
    var result = originalMethod.apply(this, args);  // ③ 在新函数里调用原方法
    console.log("方法返回: " + JSON.stringify(result));
    return result;
  };
}

三步:

  1. 保存原方法:originalMethod 指向原来的 add

  2. 替换:把descriptor.value 改成新函数

  3. 新函数里调用原方法:用apply保证this和参数正确传递

因为 descriptor 是引用类型,改 descriptor.value 就等于改了原型上的方法。

6. 完整执行流程

class MathService {
  @log
  add(a, b) { return a + b; }
}

类定义时:TypeScript 自动调用 log(MathService.prototype, "add", descriptor)

logdescriptor.value 从原 add 换成新函数

原型上的 add 现在指向新函数
调用时:math.add(5, 3) 执行的是新函数

新函数先打印参数,再调用原 add 得到8,打印返回值,最后返回8

输出结果:

调用方法: add,参数: [5,3]
方法返回: 8
计算结果: 8

7. 和 AOP 的关系
AOP(面向切面编程)的核心就是“在方法前后插入逻辑”。装饰器就是 TypeScript/JavaScript 里实现 AOP 的一种方式:

  • Spring 用动态代理实现 AOP
  • TypeScript 用装饰器替换方法实现 AOP

思想一样,手段不同。日志、权限、缓存、事务这些横切关注点,都可以用装饰器统一处理。

总结

  • 装饰器就是一个函数,TypeScript 在类定义时自动调用它。

  • 三个参数分别是:target(方法所在的原型)、propertyKey(方法名)、descriptor(方法的描述符)。

  • 核心操作是替换 descriptor.value,在原方法前后插入逻辑。

  • 原方法本身不用改,调用方也感觉不到区别。

  • 这就是AOP思想在TypeScript里的落地方式。


References

posted @ 2026-09-22 15:25  Hans599  阅读(4)  评论(0)    收藏  举报