TypeScript 装饰器是近年来前端开发中越来越重要的元编程工具。作为一名长期使用 TypeScript 进行企业级应用开发的工程师,我发现装饰器能显著提升代码的可维护性和可扩展性。本文将带你深入理解装饰器的原理、应用场景和最佳实践。
装饰器本质上是一种特殊类型的声明,可以附加到类声明、方法、访问器、属性或参数上。它的核心价值在于能够将横切关注点(如日志、验证、缓存等)从业务逻辑中分离出来,实现代码的声明式复用。
要使用装饰器,首先需要在 tsconfig.json 中启用相关配置:
json复制{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES5"
}
}
这里有几个关键点需要注意:
experimentalDecorators:必须设置为 true 才能使用装饰器语法emitDecoratorMetadata:启用元数据发射,这对依赖注入等高级用法很重要target:建议至少设置为 ES5,因为装饰器需要一些 ES5 的特性支持TypeScript 支持五种主要类型的装饰器:
| 装饰器类型 | 应用目标 | 常见用途 |
|---|---|---|
| 类装饰器 | 类声明 | 单例模式、Mixin、日志 |
| 方法装饰器 | 类方法 | 日志、性能监控、防抖/节流 |
| 属性装饰器 | 类属性 | 验证、依赖注入、元数据标记 |
| 参数装饰器 | 方法参数 | 验证、参数映射、依赖注入 |
| 访问器装饰器 | getter/setter方法 | 缓存、验证、访问控制 |
理解装饰器的执行顺序至关重要,特别是在多个装饰器组合使用时。装饰器的执行分为两个阶段:
装饰器工厂函数(即返回装饰器函数的函数)按照从上到下、从外到内的顺序执行:
typescript复制function A() {
console.log('1. 工厂 A 被调用');
return function (target: any) {
console.log('4. 装饰器 A 被执行');
};
}
function B() {
console.log('2. 工厂 B 被调用');
return function (target: any) {
console.log('3. 装饰器 B 被执行');
};
}
@A()
@B()
class Example {}
输出顺序将是:
对于类成员的装饰器,执行顺序更加复杂:
typescript复制function ClassDec() {
console.log('① 类装饰器工厂');
return function (target: any) {
console.log('⑥ 类装饰器执行');
};
}
function PropDec() {
console.log('② 属性装饰器工厂');
return function (target: any, key: string) {
console.log('④ 属性装饰器执行');
};
}
function MethodDec() {
console.log('③ 方法装饰器工厂');
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log('⑤ 方法装饰器执行');
};
}
@ClassDec()
class Example {
@PropDec()
name: string;
@MethodDec()
method() {}
}
完整执行顺序为:
当同一个声明上有多个装饰器时:
typescript复制function First() {
console.log('工厂 First');
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log('装饰器 First 执行');
};
}
function Second() {
console.log('工厂 Second');
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log('装饰器 Second 执行');
};
}
class Example {
@First()
@Second()
method() {}
}
输出顺序为:
这相当于:
typescript复制method = First(Second(
Object.getOwnPropertyDescriptor(Example.prototype, 'method')
));
| 阶段 | 顺序 | 说明 |
|---|---|---|
| 工厂函数调用 | 从上到下 | 最先执行,返回真正的装饰器函数 |
| 成员声明顺序 | 从上到下 | 属性、方法按代码声明顺序 |
| 装饰器函数应用 | 从下到上 | 后声明的先应用(类似洋葱模型) |
| 实际调用时 | 从外到内 | 运行时最外层装饰器先执行 |
记忆口诀:
code复制工厂从上走到下
装饰从下往上挂
方法调用先外层
层层包裹像穿褂
属性装饰器在类属性声明之前被声明,它接收两个参数:
typescript复制function PropertyDecorator(target: any, key: string) {
// target: 实例属性为原型对象,静态属性为构造函数
// key: 属性名字符串
}
依赖注入是属性装饰器的典型应用场景:
typescript复制import "reflect-metadata";
function Inject(target: any, key: string) {
const type = Reflect.getMetadata("design:type", target, key);
target[key] = new type();
}
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
@Inject
logger!: Logger; // ! 表示"我会确保这个属性被赋值"
createUser() {
this.logger.log("用户创建成功");
}
}
new UserService().createUser();
// 输出: [LOG] 用户创建成功
这里有几个关键点:
reflect-metadata 获取属性的类型信息! 是确定赋值断言,告诉 TypeScript 这个属性会被确保赋值属性装饰器也可以用于验证:
typescript复制function Validate(rule: { minLength?: number; pattern?: RegExp }) {
return function (target: any, key: string) {
let value: any;
Object.defineProperty(target, key, {
get: () => value,
set: (newValue: any) => {
if (rule.minLength && newValue?.length < rule.minLength) {
throw new Error(`${key} 长度不能少于 ${rule.minLength}`);
}
if (rule.pattern && !rule.pattern.test(newValue)) {
throw new Error(`${key} 格式不正确`);
}
value = newValue;
},
});
};
}
class User {
@Validate({ minLength: 3, pattern: /^[a-zA-Z]+$/ })
name!: string;
}
const user = new User();
user.name = "Jo"; // ❌ 抛出错误: name 长度不能少于 3
这种验证方式比在方法中手动验证更加优雅和可复用。
方法装饰器可以观察、修改或替换方法定义,它接收三个参数:
typescript复制function MethodDecorator(
target: any, // 实例方法:原型对象;静态方法:构造函数
key: string, // 方法名
descriptor: PropertyDescriptor, // 方法描述符
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
// 修改方法行为
return original.apply(this, args);
};
}
typescript复制function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] ${key} 被调用,参数:`, args);
const result = original.apply(this, args);
console.log(`[LOG] ${key} 返回:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(1, 2);
// [LOG] add 被调用,参数: [1, 2]
// [LOG] add 返回: 3
防抖是前端开发中常见的需求:
typescript复制function Debounce(delay: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
let timer: NodeJS.Timeout | null = null;
descriptor.value = function (...args: any[]) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => original.apply(this, args), delay);
};
};
}
class SearchBox {
query = "";
@Debounce(300)
search() {
console.log("搜索:", this.query);
}
}
缓存可以显著提升性能:
typescript复制function Cache(ttl?: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
const cache = new Map<string, { value: any; expiry: number }>();
descriptor.value = function (...args: any[]) {
const cacheKey = JSON.stringify(args);
const cached = cache.get(cacheKey);
if (cached && (!ttl || Date.now() < cached.expiry)) {
console.log("从缓存返回");
return cached.value;
}
const result = original.apply(this, args);
cache.set(cacheKey, {
value: result,
expiry: ttl ? Date.now() + ttl : Number.MAX_VALUE,
});
return result;
};
};
}
class MathService {
@Cache(5000) // 缓存5秒
fibonacci(n: number): number {
console.log("计算 fibonacci");
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
对于不稳定的操作,重试机制很有用:
typescript复制function Retry(maxAttempts: number = 3, delay: number = 1000) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: any;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await original.apply(this, args);
} catch (error) {
lastError = error;
if (attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw new Error(`${key} 失败 ${maxAttempts} 次后放弃`);
};
};
}
class ApiService {
@Retry(3, 1000)
async fetchData(url: string) {
const response = await fetch(url);
if (!response.ok) throw new Error("请求失败");
return response.json();
}
}
装饰器工厂是一个返回装饰器函数的函数,用于传递自定义参数。
typescript复制function DecoratorFactory(config: any) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
// 使用 config 定制装饰器行为
};
}
class Example {
@DecoratorFactory({ option: 'value' })
method() {}
}
多个装饰器可以组合使用:
typescript复制class UserService {
@Log()
@Validate({ name: "string" })
@Catch((error) => console.error(error))
createUser(name: string) {
// 先执行 Catch,再 Validate,最后 Log
}
}
可以创建一个组合装饰器的工具函数:
typescript复制function Compose(...decorators: PropertyDecorator[]) {
return function (
target: any,
key: string,
descriptor: PropertyDescriptor,
) {
decorators.forEach((decorator) => decorator(target, key, descriptor));
};
}
class Example {
@Compose(Log(), Validate(), Catch())
method() {}
}
typescript复制import "reflect-metadata";
const Injectable = () => (_target: any) => {};
const constructors = new Map<string, any>();
function Inject(token?: string) {
return function (target: any, key: string) {
const type = Reflect.getMetadata("design:type", target, key);
const dependencyToken = token || type.name;
Object.defineProperty(target, key, {
get() {
if (!constructors.has(dependencyToken)) {
throw new Error(`依赖 ${dependencyToken} 未注册`);
}
return constructors.get(dependencyToken);
},
set(value) {
constructors.set(dependencyToken, value);
},
});
};
}
@Injectable()
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
@Injectable()
class Database {
connect() {
console.log("[DB] 连接数据库");
}
}
class UserService {
@Inject()
logger!: Logger;
@Inject()
db!: Database;
createUser() {
this.logger.log("创建用户");
this.db.connect();
}
}
// 注册依赖
constructors.set("Logger", new Logger());
constructors.set("Database", new Database());
new UserService().createUser();
// [LOG] 创建用户
// [DB] 连接数据库
typescript复制const ROUTES: Array<{
path: string;
method: string;
handler: Function;
}> = [];
function Controller(prefix: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log(`📦 控制器注册: ${prefix}`);
}
};
};
}
function Get(path: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
ROUTES.push({
path,
method: "GET",
handler: descriptor.value,
});
};
}
function Post(path: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
ROUTES.push({
path,
method: "POST",
handler: descriptor.value,
});
};
}
@Controller("/api/users")
class UserController {
@Get("/:id")
getUser(id: string) {
return { id, name: "用户详情" };
}
@Post("/")
createUser(data: any) {
return { success: true, data };
}
}
console.log("📋 已注册的路由:", ROUTES);
typescript复制interface ColumnMetadata {
type: "string" | "number" | "boolean" | "date";
primary?: boolean;
nullable?: boolean;
length?: number;
}
const entityMetadata = new Map<Function, Map<string, ColumnMetadata>>();
function Entity(tableName: string) {
return function (constructor: Function) {
console.log(`📊 实体表名: ${tableName}`);
};
}
function Column(options: ColumnMetadata) {
return function (target: any, key: string) {
if (!entityMetadata.has(target.constructor)) {
entityMetadata.set(target.constructor, new Map());
}
entityMetadata.get(target.constructor)!.set(key, options);
};
}
@Entity("users")
class User {
@Column({ type: "number", primary: true })
id!: number;
@Column({ type: "string", length: 100 })
name!: string;
@Column({ type: "string", length: 255, nullable: true })
email!: string | null;
@Column({ type: "boolean", nullable: false })
isActive!: boolean;
}
const userMetadata = entityMetadata.get(User);
console.log("📋 User 实体元数据:", userMetadata);
typescript复制interface ValidationRule {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
custom?: (value: any) => boolean | string;
}
const validations = new Map<Object, Map<string, ValidationRule[]>>();
function Validate(rules: ValidationRule | ValidationRule[]) {
return function (target: any, key: string) {
const ruleArray = Array.isArray(rules) ? rules : [rules];
if (!validations.has(target)) {
validations.set(target, new Map());
}
validations.get(target)!.set(key, ruleArray);
let value: any;
return {
get() {
return value;
},
set(newValue: any) {
const errors: string[] = [];
for (const rule of ruleArray) {
if (rule.required && !newValue) {
errors.push(`${key} 是必填项`);
}
if (rule.minLength && newValue?.length < rule.minLength) {
errors.push(`${key} 长度不能少于 ${rule.minLength}`);
}
if (rule.maxLength && newValue?.length > rule.maxLength) {
errors.push(`${key} 长度不能超过 ${rule.maxLength}`);
}
if (rule.pattern && !rule.pattern.test(newValue)) {
errors.push(`${key} 格式不正确`);
}
if (rule.custom) {
const result = rule.custom(newValue);
if (result !== true) {
errors.push(result as string);
}
}
}
if (errors.length > 0) {
console.error(`❌ 验证失败 (${key}):`, errors.join(", "));
throw new Error(errors.join(", "));
}
value = newValue;
},
};
};
}
class RegisterForm {
@Validate({
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/,
})
username!: string;
@Validate({
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
})
email!: string;
@Validate([
{ required: true, minLength: 8 },
{
custom: (value) => /[A-Z]/.test(value) || "密码必须包含大写字母",
},
{
custom: (value) => /[a-z]/.test(value) || "密码必须包含小写字母",
},
{
custom: (value) => /[0-9]/.test(value) || "密码必须包含数字",
},
])
password!: string;
}
this绑定:确保方法调用时的上下文正确装饰器在类定义时执行,通常不会影响运行时性能。但是:
reflect-metadata)可能有性能开销在实际项目中,应该对装饰器的性能影响进行评估,特别是在大型应用中。
reflect-metadata 是装饰器的核心依赖,提供了运行时类型反射能力:
typescript复制import "reflect-metadata";
class Example {
@Reflect.metadata("design:type", String)
name: string;
@Reflect.metadata("design:paramtypes", [String, Number])
method(param1: string, param2: number) {}
}
const type = Reflect.getMetadata("design:type", Example.prototype, "name");
const paramTypes = Reflect.getMetadata("design:paramtypes", Example.prototype, "method");
console.log(type); // [Function: String]
console.log(paramTypes); // [ [Function: String], [Function: Number] ]
装饰器在继承链中的行为:
typescript复制function Log(target: any, key: string) {
console.log(`装饰器应用于 ${target.constructor.name}.${key}`);
}
class Parent {
@Log
method() {}
}
class Child extends Parent {
@Log
method() {}
}
// 输出:
// 装饰器应用于 Parent.method
// 装饰器应用于 Child.method
确保装饰器不破坏方法的 this 绑定:
typescript复制function BadDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
return original(...args); // ❌ 错误的 this 绑定
};
}
function GoodDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
return original.apply(this, args); // ✅ 正确的 this 绑定
};
}
正确处理异步方法:
typescript复制function AsyncDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
console.time(key);
const result = await original.apply(this, args);
console.timeEnd(key);
return result;
} catch (error) {
console.error(`${key} 执行失败:`, error);
throw error;
}
};
}
class Example {
@AsyncDecorator
async fetchData() {
// 异步操作
}
}
在实际项目中采用装饰器时,应该权衡其带来的便利性和潜在的风险,特别是在大型长期维护的项目中。