理解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 |
方法的描述符 | 包含 value、writable 等 |
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;
};
}
三步:
-
保存原方法:
originalMethod指向原来的add -
替换:把
descriptor.value改成新函数 -
新函数里调用原方法:用
apply保证this和参数正确传递
因为 descriptor 是引用类型,改 descriptor.value 就等于改了原型上的方法。
6. 完整执行流程
class MathService {
@log
add(a, b) { return a + b; }
}
类定义时:TypeScript 自动调用 log(MathService.prototype, "add", descriptor)
log 把 descriptor.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

浙公网安备 33010602011771号