你有没有在Angular或NestJS里见过
@Component、@Injectable这种稀奇古怪的“@符号”?它们就像给代码贴的“便利贴”,背后却能自动帮你做一堆事情。今天我们就来揭开TypeScript装饰器的神秘面纱,看看这个“贴标签”魔法到底怎么用,以及为什么它能让你少写几千行重复代码。
前言
想象你去餐厅吃饭,你在菜单上贴了个标签“@少油”,厨房看到后自动给你少放油。你又贴个“@加辣”,厨房又自动加辣。你只需要贴标签,厨房负责执行。
这就是装饰器。它是一种特殊的声明,可以附加在类、方法、属性、参数上,用来修改或增强它们的行为。你不用手动调用什么函数,只要贴上“标签”,背后的逻辑就会自动生效。
一、装饰器长啥样?先看个例子
在TypeScript里,装饰器以@expression的形式出现,expression是一个函数,会在运行时被调用。
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('调用了方法:', propertyKey);
}
class Person {
@log
sayHello() {
console.log('Hello');
}
}
const p = new Person();
p.sayHello();
// 输出:
// 调用了方法: sayHello
// Hello
你什么都没改,只是加了个@log,每次调用sayHello就会自动打印日志。这就是装饰器的魅力。
二、启用装饰器:别急,先开个开关
TypeScript的装饰器目前是实验性特性,需要在tsconfig.json里开启:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true // 可选,用于反射元数据
}
}
三、装饰器的四种类型
装饰器可以贴在四个地方:类、方法、访问器/属性、参数。每种都有不同的参数签名。
1. 类装饰器
作用在类上,通常用来修改或替换类的定义。
function addTimestamp<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
timestamp = new Date();
};
}
@addTimestamp
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new User('张三');
console.log(user); // User { name: '张三', timestamp: 2025-04-10... }
类装饰器接收一个参数:类的构造函数。你可以返回一个新类替换它,或者直接修改原型。
2. 方法装饰器
最常用,可以拦截、修改、替换方法。
function measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = performance.now();
const result = original.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} 耗时 ${end - start}ms`);
return result;
};
return descriptor;
}
class Calculator {
@measure
add(a: number, b: number) {
return a + b;
}
}
参数:
target:类的原型(静态方法则是构造函数)propertyKey:方法名descriptor:属性描述符(可以修改value、writable等)
3. 属性装饰器
作用在属性上,通常用于配合元数据做依赖注入或验证。
function format(formatStr: string) {
return function(target: any, propertyKey: string) {
let value: string;
const getter = function() {
return value;
};
const setter = function(newVal: string) {
value = formatStr.replace('%s', newVal);
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Greeting {
@format('Hello, %s')
name: string;
}
属性装饰器只能拿到目标类和属性名,不能直接修改属性值,但可以通过Object.defineProperty替换getter/setter。
4. 参数装饰器
作用在函数参数上,常用于依赖注入框架(比如Angular)。
function paramLogger(target: any, propertyKey: string, parameterIndex: number) {
console.log(`参数位置 ${parameterIndex} 被装饰了`);
}
class UserService {
getUser(@paramLogger id: number) {
return { id };
}
}
参数装饰器很少单独用,通常配合类装饰器或方法装饰器收集元数据。
四、装饰器工厂:给装饰器传参数
你看到@log、@measure这些是不带参数的。如果想让装饰器接受配置,需要再包一层函数:
function log(prefix: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`${prefix} 调用 ${propertyKey}`);
return original.apply(this, args);
};
};
}
class Test {
@log('DEBUG')
doSomething() {
console.log('执行');
}
}
这就是装饰器工厂:外层函数接收参数,内层函数是真正的装饰器。
五、多个装饰器:从下往上,从右往左
当你在同一个目标上使用多个装饰器时,它们的执行顺序是:先执行靠近目标的(从下往上),再执行外层的。
@classDecoratorA
@classDecoratorB
class MyClass {}
执行顺序:classDecoratorB 先执行,然后 classDecoratorA。
方法上的装饰器类似:先执行参数装饰器,再执行方法装饰器,最后是类装饰器(但方法装饰器本身的调用顺序是从下往上)。
六、实战:用装饰器实现权限校验
假设你要写一个类,某些方法只有管理员能调用。你可以用装饰器优雅地实现:
function adminOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
if (!this.isAdmin) {
throw new Error('无权限,需要管理员角色');
}
return original.apply(this, args);
};
}
class UserController {
isAdmin = false;
@adminOnly
deleteUser(id: number) {
console.log(`删除用户 ${id}`);
}
}
const ctrl = new UserController();
ctrl.deleteUser(1); // 报错:无权限
ctrl.isAdmin = true;
ctrl.deleteUser(1); // 成功
看,你只需要在需要权限的方法上贴个@adminOnly,逻辑自动注入。
七、装饰器的实际应用场景
- 日志记录:自动打印方法入参、返回值、耗时。
- 权限校验:检查当前用户角色。
- 数据验证:验证方法参数格式。
- 依赖注入:Angular、NestJS 里大量使用。
- 性能监控:自动记录方法执行时间。
- 重试机制:方法失败后自动重试。
八、注意事项与坑点
- 装饰器目前是实验特性,虽然Angular、NestJS等框架广泛使用,但未来ECMAScript标准可能会有所变化。
- 不能用在普通JS文件,必须在TS或Babel中启用。
- 属性装饰器不能直接修改属性值,需要通过
Object.defineProperty替换getter/setter。 - 装饰器在类定义时执行,而不是实例化时。这意味着你不能依赖实例属性(比如
this.isAdmin)来做静态分析,但可以在返回的函数中延迟读取。
九、总结:装饰器就像“代码贴纸”
- 装饰器是给类、方法、属性、参数贴的“标签”。
- 标签背后的函数会在运行时自动执行,修改目标的行为。
- 装饰器工厂可以传参,实现定制化。
- 多个装饰器从下往上执行。
- 常见用途:日志、权限、验证、注入。
学会装饰器,你就能写出更声明式、更优雅的代码。很多框架的魔法背后,其实就是这些小小的“@”符号。
如果你觉得今天的“便利贴”魔法够神奇,点个赞让更多人看到。明天我们将开启浏览器渲染原理之旅,从输入URL到页面显示,中间到底发生了什么?我们明天见!