装饰器基础
概述
装饰器(Decorator)是 TypeScript 提供的一种特殊语法,用于在类、方法、属性、参数等声明上添加元数据和修改行为。装饰器本质上是一个函数,它可以在编译时或运行时对类及其成员进行增强。装饰器在 Angular、NestJS 等框架中被广泛使用,是实现依赖注入、路由装饰、验证等功能的重要工具。
注意
装饰器目前仍处于实验性阶段(Experimental),需要在 tsconfig.json 中启用 experimentalDecorators 选项。装饰器的语法和行为可能会在未来版本中发生变化。
启用装饰器
在使用装饰器之前,需要在 TypeScript 配置文件中启用装饰器支持:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}配置说明:
experimentalDecorators: 启用装饰器语法支持emitDecoratorMetadata: 启用装饰器元数据,用于反射和依赖注入
装饰器类型
TypeScript 支持以下类型的装饰器:
- 类装饰器(Class Decorator):应用于类声明
- 方法装饰器(Method Decorator):应用于方法
- 属性装饰器(Property Decorator):应用于属性
- 参数装饰器(Parameter Decorator):应用于参数
- 访问器装饰器(Accessor Decorator):应用于 getter/setter
基本语法
装饰器使用 @ 符号作为前缀,放在被装饰的声明之前:
// 装饰器函数
function myDecorator(target: any) {
// 装饰器逻辑
}
// 使用装饰器
@myDecorator
class MyClass {
// ...
}装饰器函数签名
不同类型的装饰器有不同的函数签名:
// 类装饰器
function classDecorator(target: Function): void | Function {
// target 是类的构造函数
}
// 方法装饰器
function methodDecorator(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): void | PropertyDescriptor {
// target: 类的原型对象
// propertyKey: 方法名
// descriptor: 属性描述符
}
// 属性装饰器
function propertyDecorator(target: any, propertyKey: string): void {
// target: 类的原型对象或类构造函数
// propertyKey: 属性名
}
// 参数装饰器
function parameterDecorator(
target: any,
propertyKey: string | undefined,
parameterIndex: number
): void {
// target: 类的原型对象或类构造函数
// propertyKey: 方法名(如果是方法参数)或 undefined(如果是构造函数参数)
// parameterIndex: 参数在参数列表中的索引
}类装饰器
类装饰器应用于类声明,接收类的构造函数作为参数。
基本示例
// 定义一个简单的类装饰器
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;
}
}修改类定义
类装饰器可以返回一个新的构造函数来替换原来的类:
// 添加静态属性的装饰器
function addStaticProperty(value: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
static version = value;
};
};
}
@addStaticProperty("1.0.0")
class MyClass {
name: string;
constructor(name: string) {
this.name = name;
}
}
console.log(MyClass.version); // "1.0.0"实际应用:日志装饰器
// 为类添加日志功能的装饰器
function logClass(target: Function) {
const original = target;
// 创建一个新的构造函数
function constructor(...args: any[]) {
console.log(`Instantiating ${original.name}`);
return new original(...args);
}
// 复制原型
constructor.prototype = original.prototype;
return constructor as any;
}
@logClass
class Calculator {
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator(); // 输出: "Instantiating Calculator"方法装饰器
方法装饰器应用于类的方法,可以修改方法的行为。
基本示例
// 只读方法装饰器
function readonly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@readonly
getName(): string {
return this.name;
}
}
const person = new Person("John");
console.log(person.getName()); // "John"
// ❌ 错误:无法修改只读方法
// person.getName = function() { return "Jane"; };实际应用:性能监控装饰器
// 测量方法执行时间的装饰器
function measureTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} 执行时间: ${end - start} 毫秒`);
return result;
};
return descriptor;
}
class DataProcessor {
@measureTime
processData(data: number[]): number[] {
// 模拟数据处理
return data.map(x => x * 2);
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);
// 输出: "processData 执行时间: X 毫秒"实际应用:参数验证装饰器
// 验证方法参数的装饰器
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// 验证第一个参数是否为数字
if (args.length > 0 && typeof args[0] !== 'number') {
throw new Error(`${propertyKey} 的第一个参数必须是数字`);
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@validate
divide(a: number, b: number): number {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
}
const calc = new Calculator();
calc.divide(10, 2); // 5
// ❌ 错误:参数类型不匹配
// calc.divide("10", 2); // Error: divide 的第一个参数必须是数字属性装饰器
属性装饰器应用于类的属性,可以修改属性的行为或添加元数据。
基本示例
// 标记属性为必需的装饰器
function required(target: any, propertyKey: string) {
// 在原型上存储元数据
if (!target._required) {
target._required = [];
}
target._required.push(propertyKey);
}
class User {
@required
name: string;
@required
email: string;
age?: number; // 可选属性
constructor(name: string, email: string, age?: number) {
this.name = name;
this.email = email;
this.age = age;
}
validate(): boolean {
const required = (this.constructor as any)._required || [];
return required.every((key: string) => this[key] !== undefined);
}
}
const user = new User("John", "john@example.com");
console.log(user.validate()); // true
const invalidUser = new User("", "john@example.com");
console.log(invalidUser.validate()); // false(name 为空)实际应用:格式化装饰器
// 自动格式化字符串属性的装饰器
function format(formatFn: (value: any) => any) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
value = formatFn(newVal);
};
// 重新定义属性
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Product {
@format((value: string) => value.trim().toUpperCase())
name: string;
constructor(name: string) {
this.name = name;
}
}
const product = new Product(" laptop ");
console.log(product.name); // "LAPTOP"(自动去除空格并转为大写)参数装饰器
参数装饰器应用于构造函数或方法的参数,主要用于依赖注入和参数验证。
基本示例
// 标记参数类型的装饰器
function paramType(type: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
if (!target._paramTypes) {
target._paramTypes = [];
}
if (!target._paramTypes[parameterIndex]) {
target._paramTypes[parameterIndex] = [];
}
target._paramTypes[parameterIndex].push(type);
};
}
class Service {
constructor(
@paramType("string") name: string,
@paramType("number") age: number
) {
// ...
}
process(
@paramType("string") data: string,
@paramType("number") count: number
): void {
// ...
}
}实际应用:依赖注入标记
// 标记可注入依赖的装饰器
function inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
if (!target._injections) {
target._injections = [];
}
target._injections[parameterIndex] = token;
};
}
// 简单的依赖注入容器
class Container {
private services = new Map<string, any>();
register(token: string, service: any) {
this.services.set(token, service);
}
resolve<T>(target: new (...args: any[]) => T): T {
const injections = (target as any)._injections || [];
const args = injections.map((token: string) => this.services.get(token));
return new target(...args);
}
}
// 使用示例
class DatabaseService {
connect() {
return "Connected to database";
}
}
class UserService {
constructor(@inject("DatabaseService") private db: DatabaseService) {}
getUsers() {
return this.db.connect() + " - Users retrieved";
}
}
const container = new Container();
container.register("DatabaseService", new DatabaseService());
const userService = container.resolve(UserService);
console.log(userService.getUsers()); // "Connected to database - Users retrieved"装饰器执行顺序
当多个装饰器应用于同一个声明时,它们的执行顺序遵循特定的规则:
- 参数装饰器:从最后一个参数到第一个参数
- 方法装饰器、属性装饰器、访问器装饰器:从下到上
- 构造函数参数装饰器:从最后一个参数到第一个参数
- 类装饰器:从下到上
// 装饰器执行顺序示例
function first() {
console.log("first(): 工厂函数执行");
return function (target: any) {
console.log("first(): 装饰器应用");
};
}
function second() {
console.log("second(): 工厂函数执行");
return function (target: any) {
console.log("second(): 装饰器应用");
};
}
@first()
@second()
class Example {
// ...
}
// 输出顺序:
// first(): 工厂函数执行
// second(): 工厂函数执行
// second(): 装饰器应用
// first(): 装饰器应用装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,允许传递参数来配置装饰器行为。
基本语法
// 装饰器工厂
function decoratorFactory(config: any) {
return function (target: any) {
// 使用 config 配置装饰器
console.log(`装饰器配置: ${JSON.stringify(config)}`);
};
}
@decoratorFactory({ enabled: true, level: "info" })
class MyClass {
// ...
}实际应用:可配置的日志装饰器
// 可配置的日志装饰器工厂
function log(config?: { level?: string; prefix?: string }) {
const level = config?.level || "info";
const prefix = config?.prefix || "";
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const message = `${prefix}${propertyKey}(${args.join(", ")})`;
if (level === "info") {
console.log(`[INFO] ${message}`);
} else if (level === "warn") {
console.warn(`[WARN] ${message}`);
} else if (level === "error") {
console.error(`[ERROR] ${message}`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class Calculator {
@log({ level: "info", prefix: "计算: " })
add(a: number, b: number): number {
return a + b;
}
@log({ level: "warn" })
subtract(a: number, b: number): number {
return a - b;
}
}
const calc = new Calculator();
calc.add(5, 3); // 输出: [INFO] 计算: add(5, 3)
calc.subtract(10, 4); // 输出: [WARN] subtract(10, 4)注意事项
注意
实验性特性:装饰器目前仍处于实验阶段,语法和行为可能会发生变化。在生产环境中使用需要谨慎。
类型安全:装饰器中的类型检查可能不够严格,需要开发者自行确保类型安全。
性能影响:装饰器在编译时或运行时执行,可能会影响代码性能,特别是在大量使用装饰器的情况下。
执行时机:类装饰器在类定义时执行,方法装饰器在方法定义时执行,而不是在调用时执行。
元数据支持:某些装饰器功能(如依赖注入)需要启用
emitDecoratorMetadata选项,并可能需要额外的库支持(如reflect-metadata)。
最佳实践
明确用途:只在需要添加横切关注点(如日志、验证、缓存)时使用装饰器。
保持简单:装饰器逻辑应该简单明了,避免过度复杂的实现。
文档说明:为自定义装饰器添加清晰的文档说明,包括参数、行为和示例。
类型定义:为装饰器函数提供准确的类型定义,提高类型安全性。
测试覆盖:充分测试装饰器的各种使用场景,确保行为符合预期。
相关链接
- 类装饰器 - 深入学习类装饰器、方法装饰器、属性装饰器和参数装饰器
- 装饰器工厂 - 学习装饰器工厂、装饰器组合和元数据的使用
- 类基础 - 了解类的基础概念,这是使用装饰器的前提
- 函数类型 - 理解函数类型,有助于理解装饰器的函数签名
- TypeScript 官方文档 - 装饰器