TypeScript 装饰器(Decorator)详解
装饰器看起来很"魔法",但它的本质只有一句话:装饰器就是一个普通的函数,
@xxx只是调用它的一种语法糖。
一、@ 到底把代码变成了什么
@Logger 写在类上方,等价于把类当参数传给 Logger 函数,并用函数返回值替换原类:
二、例子 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 结果原样交回调用方
四、例子 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 最后跑
包装结构(洋葱模型):
同一个类里不同位置的执行顺序,记一个大致规律:先成员、后类;成员里先参数、再方法/属性。
@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()等
八、两个必须知道的坑
-
类声明不会被提升。装饰器在类定义的那一刻就执行,所以装饰器函数本身必须在该文件加载前就已定义。
-
tsconfig 要开开关。旧式装饰器需要在
tsconfig.json里加:
{ "compilerOptions": { "experimentalDecorators": true } }
TypeScript 5.0+ 支持了一套新的标准装饰器(去掉 experimentalDecorators 的旧语义,即 stage-3 装饰器),写法更规范但生态支持暂不完全。使用 NestJS、Angular 等框架时,仍需旧式的 experimentalDecorators: true。
一句话总结
装饰器就是"接收目标 → 返回增强版目标"的函数,@ 帮你把"传入"这一步省了。理解了这一点,框架里所有花哨的 @xxx 都不再神秘。