类装饰器详解
概述
在 TypeScript 中,装饰器可以应用于类的不同部分:类本身、类的方法、类的属性以及方法的参数。每种装饰器都有其特定的应用场景和实现方式。本文将深入探讨各种装饰器的详细用法,帮助你掌握装饰器的强大功能。
类装饰器(Class Decorator)
类装饰器应用于类声明,可以观察、修改或替换类定义。类装饰器在类定义时执行,而不是在实例化时执行。
函数签名
type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;参数说明:
target: 被装饰的类的构造函数
返回值:
- 如果返回一个构造函数,将替换原来的类
- 如果返回
void或undefined,则保持原类不变
基本用法
// 简单的类装饰器:添加元数据
function addMetadata(target: Function) {
(target as any).metadata = {
version: "1.0.0",
author: "TypeScript Team"
};
}
@addMetadata
class MyClass {
// ...
}
console.log((MyClass as any).metadata);
// { version: "1.0.0", author: "TypeScript Team" }修改类定义
类装饰器可以返回一个新的构造函数来扩展或替换原来的类:
// 添加实例方法的装饰器
function addMethod(methodName: string, method: Function) {
return function <T extends { new (...args: any[]): {} }>(target: T) {
return class extends target {
[methodName] = method;
};
};
}
@addMethod("greet", function() { return "Hello!"; })
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("John");
console.log((person as any).greet()); // "Hello!"实际应用:单例模式
// 单例装饰器:确保类只有一个实例
function singleton<T extends { new (...args: any[]): {} }>(target: T) {
let instance: InstanceType<T>;
return class extends target {
constructor(...args: any[]) {
if (instance) {
return instance;
}
super(...args);
instance = this as InstanceType<T>;
}
};
}
@singleton
class DatabaseConnection {
private connection: string;
constructor() {
this.connection = "Connected to database";
console.log("创建数据库连接");
}
query(sql: string) {
return `执行查询: ${sql}`;
}
}
const db1 = new DatabaseConnection(); // 输出: "创建数据库连接"
const db2 = new DatabaseConnection(); // 不输出,返回已有实例
console.log(db1 === db2); // true实际应用:依赖注入容器
// 注册服务到容器的装饰器
const container = new Map<string, any>();
function Injectable(token?: string) {
return function <T extends { new (...args: any[]): {} }>(target: T) {
const serviceToken = token || target.name;
container.set(serviceToken, target);
return target;
};
}
@Injectable("UserService")
class UserService {
getUsers() {
return ["Alice", "Bob", "Charlie"];
}
}
@Injectable("ProductService")
class ProductService {
getProducts() {
return ["Laptop", "Phone", "Tablet"];
}
}
// 从容器获取服务
const UserServiceClass = container.get("UserService");
const userService = new UserServiceClass();
console.log(userService.getUsers()); // ["Alice", "Bob", "Charlie"]实际应用:类验证装饰器
// 验证类实例的装饰器
function validateClass(validator: (instance: any) => boolean) {
return function <T extends { new (...args: any[]): {} }>(target: T) {
return class extends target {
constructor(...args: any[]) {
super(...args);
if (!validator(this)) {
throw new Error(`验证失败: ${target.name}`);
}
}
};
};
}
@validateClass((instance: any) => instance.name && instance.age > 0)
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const person1 = new Person("John", 30); // ✅ 成功
// const person2 = new Person("", 30); // ❌ 错误: 验证失败
// const person3 = new Person("John", -1); // ❌ 错误: 验证失败方法装饰器(Method Decorator)
方法装饰器应用于类的方法,可以观察、修改或替换方法定义。方法装饰器在方法定义时执行。
函数签名
type MethodDecorator = <T>(
target: any,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;参数说明:
target: 类的原型对象(对于实例方法)或类构造函数(对于静态方法)propertyKey: 方法名称descriptor: 属性描述符,包含方法的值、可写性、可枚举性等
返回值:
- 如果返回一个新的属性描述符,将替换原来的方法
- 如果返回
void或undefined,则保持原方法不变
基本用法
// 只读方法装饰器
function readonly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
descriptor.configurable = false;
return descriptor;
}
class Calculator {
@readonly
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
// calc.add = function() { return 0; }; // ❌ 错误: 无法修改只读方法修改方法行为
方法装饰器最常见的用途是修改方法的行为,例如添加日志、缓存、重试等功能:
// 缓存装饰器:缓存方法结果
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cacheMap = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const cacheKey = JSON.stringify(args);
if (cacheMap.has(cacheKey)) {
console.log(`缓存命中: ${propertyKey}(${args.join(", ")})`);
return cacheMap.get(cacheKey);
}
const result = originalMethod.apply(this, args);
cacheMap.set(cacheKey, result);
return result;
};
return descriptor;
}
class MathService {
@cache
expensiveCalculation(n: number): number {
console.log(`执行计算: ${n}`);
// 模拟耗时计算
return n * n;
}
}
const math = new MathService();
console.log(math.expensiveCalculation(5)); // 执行计算: 5, 输出: 25
console.log(math.expensiveCalculation(5)); // 缓存命中: expensiveCalculation(5), 输出: 25实际应用:重试装饰器
// 自动重试装饰器
function retry(maxAttempts: number = 3, delay: number = 1000) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
console.log(`${propertyKey} 第 ${attempt} 次尝试失败: ${error}`);
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError!;
};
return descriptor;
};
}
class ApiService {
@retry(3, 1000)
async fetchData(url: string): Promise<any> {
// 模拟可能失败的 API 调用
if (Math.random() > 0.5) {
throw new Error("网络错误");
}
return { data: "成功获取数据" };
}
}
const api = new ApiService();
api.fetchData("https://api.example.com/data").then(console.log).catch(console.error);实际应用:权限检查装饰器
// 权限检查装饰器
function requirePermission(permission: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const user = (this as any).currentUser;
if (!user || !user.permissions.includes(permission)) {
throw new Error(`需要权限: ${permission}`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class DocumentService {
currentUser: { permissions: string[] } | null = null;
@requirePermission("read")
readDocument(id: string) {
return `读取文档 ${id}`;
}
@requirePermission("write")
writeDocument(id: string, content: string) {
return `写入文档 ${id}: ${content}`;
}
@requirePermission("delete")
deleteDocument(id: string) {
return `删除文档 ${id}`;
}
}
const service = new DocumentService();
service.currentUser = { permissions: ["read", "write"] };
console.log(service.readDocument("doc1")); // ✅ 成功
console.log(service.writeDocument("doc1", "content")); // ✅ 成功
// service.deleteDocument("doc1"); // ❌ 错误: 需要权限: delete实际应用:防抖和节流装饰器
// 防抖装饰器:延迟执行,如果在延迟期间再次调用则重新计时
function debounce(delay: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let timeoutId: NodeJS.Timeout;
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
originalMethod.apply(this, args);
}, delay);
};
return descriptor;
};
}
// 节流装饰器:限制执行频率,在指定时间内最多执行一次
function throttle(delay: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let lastCallTime = 0;
descriptor.value = function (...args: any[]) {
const now = Date.now();
if (now - lastCallTime >= delay) {
lastCallTime = now;
return originalMethod.apply(this, args);
}
};
return descriptor;
};
}
class SearchService {
@debounce(300)
search(query: string) {
console.log(`搜索: ${query}`);
}
@throttle(1000)
trackEvent(event: string) {
console.log(`追踪事件: ${event}`);
}
}属性装饰器(Property Decorator)
属性装饰器应用于类的属性,可以观察或修改属性的行为。属性装饰器在属性定义时执行。
函数签名
type PropertyDecorator = (
target: any,
propertyKey: string | symbol
) => void;参数说明:
target: 类的原型对象(对于实例属性)或类构造函数(对于静态属性)propertyKey: 属性名称
注意:属性装饰器不能直接修改属性的值,但可以通过 Object.defineProperty 重新定义属性。
基本用法
// 标记属性的装饰器
function logProperty(target: any, propertyKey: string) {
let value: any;
const getter = function () {
console.log(`获取 ${propertyKey}: ${value}`);
return value;
};
const setter = function (newVal: any) {
console.log(`设置 ${propertyKey}: ${newVal}`);
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
name: string;
constructor(name: string) {
this.name = name; // 输出: "设置 name: ..."
}
}
const person = new Person("John");
console.log(person.name); // 输出: "获取 name: John", 然后输出: "John"实际应用:属性验证装饰器
// 属性验证装饰器
function validate(validator: (value: any) => boolean, errorMessage?: string) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
if (!validator(newVal)) {
throw new Error(errorMessage || `${propertyKey} 验证失败`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class User {
@validate((v: string) => v.length >= 3, "用户名至少 3 个字符")
username: string;
@validate((v: number) => v > 0 && v < 150, "年龄必须在 0-150 之间")
age: number;
@validate((v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), "邮箱格式不正确")
email: string;
constructor(username: string, age: number, email: string) {
this.username = username;
this.age = age;
this.email = email;
}
}
const user1 = new User("john", 30, "john@example.com"); // ✅ 成功
// const user2 = new User("jo", 30, "john@example.com"); // ❌ 错误: 用户名至少 3 个字符
// const user3 = new User("john", 200, "john@example.com"); // ❌ 错误: 年龄必须在 0-150 之间实际应用:属性格式化装饰器
// 属性格式化装饰器
function format(formatter: (value: any) => any) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
value = formatter(newVal);
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Product {
@format((v: string) => v.trim().toUpperCase())
name: string;
@format((v: number) => Math.round(v * 100) / 100) // 保留两位小数
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
const product = new Product(" laptop ", 99.999);
console.log(product.name); // "LAPTOP"
console.log(product.price); // 100实际应用:属性监听装饰器
// 属性变化监听装饰器
function watch(callback: (oldValue: any, newValue: any) => void) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
const oldValue = value;
value = newVal;
callback(oldValue, newVal);
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Observable {
@watch((oldVal, newVal) => {
console.log(`值从 ${oldVal} 变为 ${newVal}`);
})
count: number = 0;
}
const obs = new Observable();
obs.count = 1; // 输出: "值从 0 变为 1"
obs.count = 2; // 输出: "值从 1 变为 2"参数装饰器(Parameter Decorator)
参数装饰器应用于构造函数或方法的参数,主要用于依赖注入和参数验证。参数装饰器在参数定义时执行。
函数签名
type ParameterDecorator = (
target: any,
propertyKey: string | symbol | undefined,
parameterIndex: number
) => void;参数说明:
target: 类的原型对象(对于方法参数)或类构造函数(对于构造函数参数)propertyKey: 方法名称(对于方法参数)或undefined(对于构造函数参数)parameterIndex: 参数在参数列表中的索引(从 0 开始)
基本用法
// 标记参数的装饰器
function logParameter(target: any, propertyKey: string | undefined, parameterIndex: number) {
const metadataKey = `__log_${propertyKey || "constructor"}_parameters`;
if (!target[metadataKey]) {
target[metadataKey] = [];
}
target[metadataKey].push(parameterIndex);
}
class Service {
constructor(
@logParameter name: string,
@logParameter age: number
) {
// ...
}
process(
@logParameter data: string,
@logParameter count: number
): void {
// ...
}
}实际应用:依赖注入
// 依赖注入装饰器
const metadataKey = Symbol("injections");
function inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
const injections = Reflect.getMetadata(metadataKey, target) || {};
if (!injections[propertyKey || "constructor"]) {
injections[propertyKey || "constructor"] = [];
}
injections[propertyKey || "constructor"][parameterIndex] = token;
Reflect.defineMetadata(metadataKey, injections, target);
};
}
// 简单的依赖注入容器
class Container {
private services = new Map<string, any>();
register<T>(token: string, service: T) {
this.services.set(token, service);
}
resolve<T>(target: new (...args: any[]) => T): T {
const injections = Reflect.getMetadata(metadataKey, target) || {};
const constructorInjections = injections["constructor"] || [];
const args = constructorInjections.map((token: string) => this.services.get(token));
return new target(...args);
}
}
// 使用示例
class DatabaseService {
connect() {
return "数据库已连接";
}
}
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
constructor(
@inject("DatabaseService") private db: DatabaseService,
@inject("LoggerService") private logger: LoggerService
) {}
getUsers() {
this.logger.log("获取用户列表");
return this.db.connect() + " - 用户列表";
}
}
const container = new Container();
container.register("DatabaseService", new DatabaseService());
container.register("LoggerService", new LoggerService());
const userService = container.resolve(UserService);
console.log(userService.getUsers());
// 输出: [LOG] 获取用户列表
// 输出: 数据库已连接 - 用户列表实际应用:参数验证
// 参数验证装饰器
function validateParam(validator: (value: any) => boolean, errorMessage?: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
const metadataKey = `__validate_${propertyKey || "constructor"}_parameters`;
if (!target[metadataKey]) {
target[metadataKey] = [];
}
target[metadataKey][parameterIndex] = { validator, errorMessage };
};
}
// 方法装饰器:在方法执行前验证参数
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const validators = (target as any)[`__validate_${propertyKey}_parameters`] || [];
descriptor.value = function (...args: any[]) {
for (let i = 0; i < args.length; i++) {
const validatorInfo = validators[i];
if (validatorInfo && !validatorInfo.validator(args[i])) {
throw new Error(validatorInfo.errorMessage || `参数 ${i} 验证失败`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@validate
divide(
@validateParam((v: number) => typeof v === "number" && !isNaN(v), "第一个参数必须是数字")
a: number,
@validateParam((v: number) => v !== 0, "除数不能为零")
b: number
): number {
return a / b;
}
}
const calc = new Calculator();
console.log(calc.divide(10, 2)); // 5
// calc.divide("10", 2); // ❌ 错误: 第一个参数必须是数字
// calc.divide(10, 0); // ❌ 错误: 除数不能为零装饰器组合使用
在实际开发中,经常需要组合使用多个装饰器来实现复杂的功能:
// 组合多个装饰器的示例
class ApiController {
@requirePermission("read")
@cache
@log({ level: "info" })
async getData(id: string) {
// 方法执行顺序:
// 1. 权限检查
// 2. 缓存检查
// 3. 日志记录
// 4. 执行实际逻辑
return { id, data: "some data" };
}
@requirePermission("write")
@validate
@log({ level: "warn" })
async updateData(
@validateParam((v: string) => v.length > 0, "ID 不能为空")
id: string,
@validateParam((v: any) => v !== null && v !== undefined, "数据不能为空")
data: any
) {
return { id, data, updated: true };
}
}注意事项
注意
执行顺序:多个装饰器的执行顺序是从下到上(对于类和方法装饰器),从最后一个参数到第一个参数(对于参数装饰器)。
类型安全:装饰器中的类型检查可能不够严格,需要开发者自行确保类型安全。
性能影响:装饰器在定义时执行,可能会影响类的初始化性能。
元数据支持:某些高级功能(如依赖注入)需要启用
emitDecoratorMetadata并可能需要reflect-metadata库。静态方法:对于静态方法,
target参数是类构造函数而不是原型对象。
最佳实践
单一职责:每个装饰器应该只负责一个功能,保持简单和可复用。
组合优于继承:使用装饰器组合来实现复杂功能,而不是创建复杂的装饰器。
文档说明:为自定义装饰器添加清晰的文档,说明参数、行为和示例。
类型定义:为装饰器提供准确的类型定义,提高类型安全性。
测试覆盖:充分测试装饰器的各种使用场景,包括边界情况。
相关链接
- 装饰器基础 - 装饰器的基本概念和语法
- 装饰器工厂 - 学习装饰器工厂、装饰器组合和元数据的使用
- 类基础 - 了解类的基本概念
- 访问修饰符 - 了解类的访问控制
- TypeScript 官方文档 - 装饰器