目录
第八章 装饰器
装饰器(Decorators)是TypeScript的一项实验性特性,它提供了一种以声明式方式添加注解和元编程语法的方法。装饰器可以附加到类声明、方法、访问器、属性或参数上,用于修改类的行为或添加元数据。
8.1 装饰器基础
装饰器本质上是一个函数,它在运行时被调用,被装饰的声明信息作为参数传入。要使用装饰器,必须在tsconfig.json中启用experimentalDecorators选项。
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "ES5"
}
}
装饰器使用@expression的形式,其中expression必须是一个函数,在运行时会被调用,传入被装饰的声明信息。
8.2 装饰器分类
TypeScript支持以下几种类型的装饰器:
8.2.1 类装饰器
类装饰器应用于类构造函数,可以用来监视、修改或替换类定义。
// 基础类装饰器
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;
}
}
类装饰器的签名:
declare type ClassDecorator = <TFunction extends Function>( target: TFunction ) => TFunction | void;
如果类装饰器返回一个值,它会使用返回的构造函数替换类声明。
// 返回新构造函数的类装饰器
function classDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
newProperty = "new property";
hello = "override";
};
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
// 输出包含newProperty和修改后的hello
8.2.2 方法装饰器
方法装饰器声明在方法的属性描述符之前,可以用来监视、修改或替换方法定义。
// 方法装饰器
function enumerable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
方法装饰器的签名:
declare type MethodDecorator = <T>( target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T> ) => TypedPropertyDescriptor<T> | void;
方法装饰器可以访问和修改属性描述符,控制方法的可枚举性、可配置性和可写性。
8.2.3 访问器装饰器
访问器装饰器应用于访问器的属性描述符,可以用来监视、修改或替换访问器定义。
// 访问器装饰器
function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
注意:TypeScript不允许同时装饰一个成员的get和set访问器。相反,所有装饰器必须应用于访问器的第一个文档顺序指定的访问器。
8.2.4 属性装饰器
属性装饰器声明在属性声明之前,用于添加元数据或执行其他操作。
// 属性装饰器
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
属性装饰器的签名:
declare type PropertyDecorator = ( target: Object, propertyKey: string | symbol ) => void;
8.2.5 参数装饰器
参数装饰器声明在参数声明之前,用于监视方法参数。
// 参数装饰器
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey
);
}
function validate(
target: any,
propertyName: string,
descriptor: TypedPropertyDescriptor<Function>
) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName
);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (
parameterIndex >= arguments.length ||
arguments[parameterIndex] === undefined
) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
8.3 装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,允许自定义装饰器的行为。
// 装饰器工厂
function log(prefix: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`${prefix} - Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`${prefix} - Result:`, result);
return result;
};
};
}
class Calculator {
@log("DEBUG")
add(a: number, b: number): number {
return a + b;
}
@log("INFO")
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.add(2, 3);
calc.multiply(4, 5);
8.4 装饰器组合
多个装饰器可以应用于一个声明,按照从下到上、从右到左的顺序执行。
// 装饰器组合
function first() {
console.log("first(): factory evaluated");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// 输出顺序:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
8.5 元数据反射
TypeScript支持通过reflect-metadata库使用元数据反射API。
import "reflect-metadata";
// 定义元数据
class MyClass {
@Reflect.metadata("custom:key", "custom value")
myMethod() {}
}
// 读取元数据
const obj = new MyClass();
const metadataValue = Reflect.getMetadata("custom:key", obj, "myMethod");
console.log(metadataValue); // "custom value"
// 类级别的元数据
@Reflect.metadata("class:key", "class value")
class DecoratedClass {}
console.log(Reflect.getMetadata("class:key", DecoratedClass)); // "class value"
8.6 实际应用场景
8.6.1 日志记录装饰器
function logExecutionTime(
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} execution time: ${end - start} milliseconds`);
return result;
};
}
class DataProcessor {
@logExecutionTime
processLargeDataset(data: number[]): number[] {
// 模拟耗时操作
return data.map(x => x * 2).filter(x => x > 10);
}
}
8.6.2 缓存装饰器
function memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Returning cached result");
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
}
class Fibonacci {
@memoize
calculate(n: number): number {
if (n < 2) return n;
return this.calculate(n - 1) + this.calculate(n - 2);
}
}
8.6.3 授权装饰器
function requireRole(role: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUser = (this as any).currentUser;
if (!currentUser || currentUser.role !== role) {
throw new Error(`Access denied. Required role: ${role}`);
}
return originalMethod.apply(this, args);
};
};
}
class AdminPanel {
currentUser = { role: "admin" };
@requireRole("admin")
deleteUser(userId: string) {
console.log(`Deleting user: ${userId}`);
}
@requireRole("superadmin")
deleteSystem() {
console.log("Deleting system...");
}
}
8.6.4 依赖注入装饰器
// 简单的依赖注入容器
class Container {
private services = new Map();
register(token: string, implementation: any) {
this.services.set(token, implementation);
}
resolve(token: string) {
return this.services.get(token);
}
}
const container = new Container();
// 注入装饰器
function inject(token: string) {
return function (target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: () => container.resolve(token),
enumerable: true,
configurable: true
});
};
}
// 服务定义
interface ILogger {
log(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
// 注册服务
container.register("ILogger", new ConsoleLogger());
// 使用注入
class UserService {
@inject("ILogger")
logger!: ILogger;
createUser(name: string) {
this.logger.log(`Creating user: ${name}`);
return { id: 1, name };
}
}
8.7 装饰器与类型系统
装饰器可以影响类型推断,需要注意类型安全。
// 确保装饰器保持类型安全
function validateType<T>(validator: (value: any) => value is T) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!validator(args[0])) {
throw new TypeError(`Invalid argument type for ${propertyKey}`);
}
return originalMethod.apply(this, args);
};
};
}
// 类型守卫
function isString(value: any): value is string {
return typeof value === "string";
}
class StringProcessor {
@validateType(isString)
process(value: string): string {
return value.toUpperCase();
}
}
8.8 最佳实践
1. 谨慎使用实验性功能:装饰器仍然是实验性功能,API可能会变化。
2. 保持装饰器简单:装饰器应该专注于单一职责,避免过于复杂。
3. 文档化装饰器行为:清楚地说明装饰器对类或方法的影响。
4. 考虑性能影响:装饰器在运行时增加开销,在性能敏感场景要谨慎。
5. 使用装饰器组合:通过组合小装饰器构建复杂功能,而不是创建大而全的装饰器。
6. 类型安全:确保装饰器不会破坏类型系统的完整性。
8.9 小结
装饰器是TypeScript强大的元编程工具,它允许以声明式方式修改类和成员的行为。通过合理使用类装饰器、方法装饰器、访问器装饰器、属性装饰器和参数装饰器,可以实现日志记录、缓存、验证、依赖注入等横切关注点的分离。
虽然装饰器仍是实验性功能,但它已经被许多框架(如Angular、NestJS)广泛采用。理解装饰器的工作原理和应用场景,对于掌握现代TypeScript开发至关重要。
