装饰器
装饰器(Decorator)是一种特殊的声明,它允许我们在类、方法、属性等定义上附加元数据或修改其行为。装饰器在很多框架中都有广泛应用,比如Angular、NestJS等。TypeScript很早就支持了装饰器语法,但早期版本的装饰器是基于Stage 2阶段的提案实现的,属于实验性特性。TypeScript 5.0正式引入了符合ECMAScript Stage 3提案的新版装饰器,这个版本的装饰器无需额外配置即可使用,语法和行为也与旧版有所不同。本篇笔记将介绍TypeScript 5.0新版装饰器的用法。
新旧装饰器的区别
在TypeScript 5.0之前,使用装饰器需要在tsconfig.json中如下配置。
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
新版装饰器则无需任何配置即可直接使用,两个版本的装饰器在语法和运行时行为上存在差异,它们并不兼容!如果项目中使用了依赖旧版装饰器的框架(如Angular、NestJS等),仍需保持旧版配置,不可混用。两个版本的装饰器主要区别如下。
| 特性 | 旧版装饰器 | 新版装饰器 |
|---|---|---|
| 提案阶段 | Stage 2 | Stage 3 |
| 配置要求 | 需要experimentalDecorators |
无需配置 |
| 参数签名 | 因装饰目标不同而各异 | 统一的(target, context)签名 |
| 元数据支持 | 需要reflect-metadata |
原生支持metadata |
| 执行顺序 | 自下而上 | 自下而上(与旧版一致) |
装饰器的基本语法
装饰器本质上是一个函数,它接收被装饰的目标和上下文信息作为参数,下面是一个最简单的类装饰器示例,我们装饰了一个类的构造函数。
function LogClass(target: Function, context: ClassDecoratorContext) {
console.log(`Class ${context.name} is decorated`);
}
@LogClass
class MyClass {
constructor() {
console.log("MyClass instance created");
}
}
const instance = new MyClass();
运行上面的代码,输出结果如下。
Class MyClass is decorated
MyClass instance created
注意这里装饰器在类定义时就会执行,而不是在实例化时执行。
类装饰器
类装饰器用于装饰类声明,它可以用来修改类的行为或添加额外的功能。
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<T extends Constructor>(
target: T,
context: ClassDecoratorContext
) {
return class extends target {
createdAt = new Date();
};
}
@Timestamped
class Document {
title: string;
constructor(title: string) {
this.title = title;
}
}
const doc = new Document("My Document");
console.log(doc.title);
console.log((doc as any).createdAt);
上面例子中,Timestamped装饰器为类添加了一个createdAt属性,记录实例创建的时间。
方法装饰器
方法装饰器用于装饰类的方法,常用于日志记录、性能监控、权限检查等场景。
function Log(
target: Function,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`Calling ${methodName} with args:`, args);
const result = target.apply(this, args);
console.log(`Method ${methodName} returned:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
运行输出结果如下。
Calling add with args: [2, 3]
Method add returned: 5
属性装饰器
属性装饰器用于装饰类的属性,可以用来实现属性验证、默认值设置等功能。
function DefaultValue(value: any) {
return function (
target: undefined,
context: ClassFieldDecoratorContext
) {
return function (initialValue: any) {
return initialValue ?? value;
};
};
}
class Settings {
@DefaultValue("guest")
username: string;
@DefaultValue(8080)
port: number;
}
const settings = new Settings();
console.log(settings.username); // 输出: guest
console.log(settings.port); // 输出: 8080
注意新版属性装饰器的target参数是undefined,这与旧版装饰器不同。
访问器装饰器
访问器装饰器用于装饰类的getter和setter。
function Validate(
target: Function,
context: ClassGetterDecoratorContext | ClassSetterDecoratorContext
) {
if (context.kind === "setter") {
return function (this: any, value: any) {
if (typeof value !== "number" || value < 0) {
throw new Error("Value must be a non-negative number");
}
target.call(this, value);
};
}
return target;
}
class Product {
private _price: number = 0;
get price(): number {
return this._price;
}
@Validate
set price(value: number) {
this._price = value;
}
}
const product = new Product();
product.price = 100; // 正常
console.log(product.price); // 输出: 100
try {
product.price = -10; // 抛出错误
} catch (e) {
console.error(e.message); // 输出: Value must be a non-negative number
}
装饰器工厂
装饰器工厂是一个返回装饰器函数的高阶函数,它允许我们在使用装饰器时传递参数。
function Retry(maxAttempts: number, delay: number) {
return function (
target: Function,
context: ClassMethodDecoratorContext
) {
return async function (this: any, ...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await target.apply(this, args);
} catch (error) {
lastError = error as Error;
console.log(`Attempt ${attempt} failed, retrying...`);
if (attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError!;
};
};
}
class ApiClient {
private failCount = 0;
@Retry(3, 1000)
async fetchData(): Promise<string> {
this.failCount++;
if (this.failCount < 3) {
throw new Error("Network error");
}
return "Data fetched successfully";
}
}
const client = new ApiClient();
client.fetchData().then(console.log).catch(console.error);
装饰器组合
多个装饰器可以同时应用于同一个目标,它们的执行顺序是自下而上(从最接近目标的装饰器开始)。
function First(
target: Function,
context: ClassMethodDecoratorContext
) {
console.log("First decorator evaluated");
return function (this: any, ...args: any[]) {
console.log("First decorator executed");
return target.apply(this, args);
};
}
function Second(
target: Function,
context: ClassMethodDecoratorContext
) {
console.log("Second decorator evaluated");
return function (this: any, ...args: any[]) {
console.log("Second decorator executed");
return target.apply(this, args);
};
}
class Example {
@First
@Second
method() {
console.log("Method executed");
}
}
const example = new Example();
example.method();
输出顺序如下。
Second decorator evaluated
First decorator evaluated
First decorator executed
Second decorator executed
Method executed
装饰器的求值顺序是自下而上的,但包装后的函数执行顺序是自上而下的(类似洋葱模型)。
上下文对象
新版装饰器的第二个参数是上下文对象(context),它提供了关于被装饰目标的元信息。
interface DecoratorContext {
kind: "class" | "method" | "getter" | "setter" | "field" | "accessor";
name: string | symbol;
static: boolean;
private: boolean;
access?: {
get?(): unknown;
set?(value: unknown): void;
};
addInitializer?(initializer: () => void): void;
metadata: DecoratorMetadata;
}
其中addInitializer方法允许我们注册一个初始化函数,它会在类实例化时执行。
function AutoBind(
target: Function,
context: ClassMethodDecoratorContext
) {
const methodName = context.name;
context.addInitializer(function (this: any) {
this[methodName] = this[methodName].bind(this);
});
return target;
}
class Button {
label = "Click me";
@AutoBind
handleClick() {
console.log(`Button: ${this.label}`);
}
}
const button = new Button();
const handler = button.handleClick;
handler(); // 输出: Button: Click me(this已正确绑定)
装饰器的实际应用场景
装饰器在实际开发中有很多应用场景:
- 日志记录:自动记录方法的调用和返回值。
- 性能监控:测量方法的执行时间。
- 缓存:缓存方法的返回结果。
- 权限验证:检查用户权限后再执行方法。
- 依赖注入:自动注入依赖项。
- 数据验证:验证方法参数或属性值。
- 单例模式:确保类只有一个实例。
下面是一个缓存装饰器的示例。
function Memoize(
target: Function,
context: ClassMethodDecoratorContext
) {
const cache = new Map<string, any>();
return function (this: any, ...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Cache hit");
return cache.get(key);
}
console.log("Cache miss");
const result = target.apply(this, args);
cache.set(key, result);
return result;
};
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const math = new MathService();
console.log(math.fibonacci(10)); // 第一次计算
console.log(math.fibonacci(10)); // 从缓存获取