TypeScript 装饰器:@decorator 语法与元数据
装饰器是 TypeScript 提供的一种强大语法糖,它允许你在不修改原代码的情况下,为类、方法、属性或参数添加额外功能。想象一下给代码贴标签——你可以在代码定义的地方"挂载"一些额外行为,这些行为在程序运行时自动触发。
1. 装饰器的基本概念
装饰器的本质是一个函数,它接收目标对象作为参数,在函数体内可以执行任意逻辑。以 @ 符号开头,放在要装饰的目标之前。
@decorator
class MyClass {}
启用装饰器功能需要在 tsconfig.json 中配置:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
2. 类装饰器
类装饰器作用于类的构造函数,它的参数是被装饰类的构造函数本身。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
上述代码中,@sealed 装饰器锁定了类的构造函数和原型,防止后续添加或删除属性。定义装饰器函数后,直接在类名上方使用 @ 符号即可生效。
3. 装饰器工厂
如果你需要向装饰器传递参数,可以使用"装饰器工厂"——一个返回装饰器函数的函数。
function color(value: string) {
return function(target: Function) {
target.prototype.color = value;
};
}
@color("blue")
class Car {
brand: string;
}
工厂函数 color 接收参数 value,返回的匿名函数才是真正的装饰器。调用时,外层函数接收配置参数,内层函数接收被装饰目标。
这种模式让装饰器变得灵活:同一个装饰器可以根据不同参数产生不同效果。
4. 方法装饰器
方法装饰器应用于类的方法,它的参数包括:目标类、属性名、以及属性的描述符。
function enumerable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Project {
name: string;
constructor(name: string) {
this.name = name;
}
@enumerable(true)
getName() {
return this.name;
}
}
@enumerable(true) 将方法设为可枚举。注意:属性描述符 PropertyDescriptor 包含 value、writable、enumerable、configurable 等属性,你可以修改它们来改变方法的默认行为。
5. 属性装饰器
属性装饰器接收两个参数:目标类和属性名。与方法装饰器不同,它不接收属性描述符,因为属性还没有被初始化。
function format(formatStr: string) {
return function(target: any, propertyKey: string) {
let value: string;
Object.defineProperty(target, propertyKey, {
get: function() {
return formatStr + value;
},
set: function(newValue: string) {
value = newValue;
},
enumerable: true,
configurable: true
});
};
}
class User {
@format("User: ")
name: string;
}
const user = new User();
user.name = "Alice";
console.log(user.name); // 输出: User: Alice
这个例子展示了属性装饰器的经典用法:拦截属性的 getter 和 setter,在访问或赋值时自动添加前缀。
6. 参数装饰器
参数装饰器用于装饰方法的参数,接收三个参数:目标类、方法名、以及参数在函数签名中的索引。
function required(target: any, propertyKey: string, parameterIndex: number) {
const existingRequiredParameters: number[] =
Reflect.getMetadata("requiredParams", target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
"requiredParams",
existingRequiredParameters,
target,
propertyKey
);
}
class Person {
greet(@required message: string) {
console.log(message);
}
}
参数装饰器通常与元数据配合使用,用于标记哪些参数是必需的。
7. 装饰器执行顺序
当多个装饰器同时作用于同一个目标时,执行顺序遵循以下规则:
- 从上到下求值装饰器工厂
- 从下到上执行装饰器函数
function first() {
console.log("factory1");
return function(target: any) {
console.log("decorator1");
};
}
function second() {
console.log("factory2");
return function(target: any) {
console.log("decorator2");
};
}
@first()
@second()
class Example {}
输出顺序是:factory1 -> factory2 -> decorator2 -> decorator1。记住:求值阶段按书写顺序从上到下,执行阶段按相反顺序从下到上。
8. 反射元数据与 reflect-metadata
装饰器的一大用途是配合反射系统存储和读取元数据。首先需要安装 reflect-metadata:
npm install reflect-metadata
在代码入口处引入:
import "reflect-metadata";
现在你可以使用 Reflect.defineMetadata 和 Reflect.getMetadata:
import "reflect-metadata";
function Column(name: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata("columnName", name, target, propertyKey);
};
}
class User {
@Column("user_name")
name: string;
}
const meta = Reflect.getMetadata("columnName", User.prototype, "name");
console.log(meta); // 输出: user_name
元数据机制让装饰器能够在运行时"记住"一些信息,这对于依赖注入、ORM 映射、参数验证等场景至关重要。
9. 实际应用场景
9.1 自动验证参数
结合参数装饰器和元数据,可以实现自动参数验证:
import "reflect-metadata";
function MinLength(min: number) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata("minLength", min, target, propertyKey);
};
}
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const minLength = Reflect.getMetadata("minLength", target, propertyKey);
for (const arg of args) {
if (typeof arg === "string" && arg.length < minLength) {
throw new Error(`Argument too short. Minimum length: ${minLength}`);
}
}
return originalMethod.apply(this, args);
};
}
class Form {
@MinLength(3)
@validate
setUsername(name: string) {
console.log(`Username set to: ${name}`);
}
}
const form = new Form();
form.setUsername("ab"); // 抛出错误:Argument too short. Minimum length: 3
form.setUsername("abc"); // 正常执行
9.2 依赖注入容器
装饰器还可以实现简易的依赖注入:
const Injectable = () => (target: any) => target;
const Container = {
services: new Map<string, any>(),
register(key: string, value: any) {
this.services.set(key, value);
},
resolve(target: any): any {
const token = Reflect.getMetadata("injectToken", target);
return this.services.get(token);
}
};
function Inject(token: string) {
return function(target: any, propertyKey: string | symbol, index?: number) {
Reflect.defineMetadata("injectToken", token, target);
};
}
@Injectable()
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
@Inject("Logger")
logger!: Logger;
createUser(name: string) {
this.logger.log(`Creating user: ${name}`);
}
}
Container.register("Logger", new Logger());
const userService = new UserService();
userService.createUser("Bob");
10. 注意事项与最佳实践
装饰器目前处于 TC39 提案的 Stage 3 阶段,生产环境中使用需谨慎评估。如果你的项目依赖装饰器实现核心功能,建议锁定 TypeScript 版本以确保编译结果稳定。
装饰器不应用于业务逻辑的"副作用"——它们更适合基础设施层面的横切关注点,如日志、验证、缓存、权限控制等。将业务逻辑与装饰器逻辑分离,可以保持代码的可读性和可维护性。

暂无评论,快来抢沙发吧!