1. TypeScript装饰器核心概念解析
装饰器(Decorator)是TypeScript中一种特殊的语法结构,它本质上就是一个高阶函数,通过@符号附加到类、方法、属性或参数上,用于修改或扩展它们的行为。这种设计模式在Angular、NestJS等现代前端框架中被广泛使用。
装饰器目前仍是ECMAScript的提案阶段特性,TypeScript通过实验性功能提供了实现。使用时需要在tsconfig.json中明确启用。
装饰器之所以强大,是因为它提供了一种声明式编程的方式。我们可以把横切关注点(如日志、权限校验、性能监控等)从业务逻辑中抽离出来,使代码更加模块化和可维护。
2. 装饰器类型与基础用法
2.1 类装饰器:增强类的能力
类装饰器是最基础的类型,它接收一个参数:被装饰类的构造函数。下面是一个更实用的例子,展示如何用类装饰器实现单例模式:
typescript复制function Singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: T;
return class extends constructor {
constructor(...args: any[]) {
if (!instance) {
super(...args);
instance = this;
}
return instance;
}
};
}
@Singleton
class DatabaseConnection {
constructor() {
console.log('创建数据库连接');
}
}
const conn1 = new DatabaseConnection(); // 输出:创建数据库连接
const conn2 = new DatabaseConnection(); // 无输出
console.log(conn1 === conn2); // true
这个装饰器通过闭包保存实例引用,确保类只会被实例化一次。在实际项目中,这种模式常用于数据库连接、配置管理等场景。
2.2 方法装饰器:最常用的装饰器类型
方法装饰器接收三个参数:
- 对于静态成员是类的构造函数,对于实例成员是类的原型
- 成员名称
- 属性描述符(PropertyDescriptor)
下面是一个更全面的方法装饰器示例,实现方法执行时间统计:
typescript复制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).toFixed(2)}ms`);
return result;
};
return descriptor;
}
class Calculator {
@measureTime
static fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Calculator.fibonacci(30); // 输出执行时间
注意:方法装饰器中修改descriptor.value时,需要使用function而非箭头函数,以确保正确的this绑定。
3. 高级装饰器模式
3.1 属性装饰器:元编程的强大工具
属性装饰器接收两个参数:类的原型(或构造函数)和属性名。虽然不能直接修改属性描述符,但可以用于元数据编程:
typescript复制import 'reflect-metadata';
function DefaultValue(value: any) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata('design:default', value, target, propertyKey);
};
}
class User {
@DefaultValue('anonymous')
username: string;
@DefaultValue(0)
score: number;
constructor() {
this.username = Reflect.getMetadata('design:default', this, 'username') || '';
this.score = Reflect.getMetadata('design:default', this, 'score') || 0;
}
}
const user = new User();
console.log(user.username); // "anonymous"
console.log(user.score); // 0
这个例子结合了reflect-metadata库,实现了属性的默认值设置。在实际项目中,这种模式常用于表单验证、ORM映射等场景。
3.2 装饰器工厂:参数化装饰器
装饰器工厂是返回装饰器函数的函数,它允许我们传递自定义参数:
typescript复制function Validate(regex: RegExp, message: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(value: string) {
if (!regex.test(value)) {
throw new Error(message);
}
return originalMethod.call(this, value);
};
};
}
class FormService {
@Validate(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, '无效的邮箱格式')
submitEmail(email: string) {
console.log(`提交邮箱: ${email}`);
}
}
const service = new FormService();
service.submitEmail('test@example.com'); // 正常
service.submitEmail('invalid-email'); // 抛出错误
这个装饰器工厂创建了一个参数验证装饰器,可以复用在不同场景中验证不同格式的数据。
4. 装饰器执行顺序与组合
4.1 多个装饰器的执行顺序
当多个装饰器应用于同一个声明时,它们的执行顺序遵循以下规则:
- 参数装饰器 → 方法装饰器 → 访问器装饰器 → 属性装饰器 → 类装饰器
- 同一类型的装饰器,从下往上执行(离声明最近的先执行)
typescript复制function DecoratorA() {
console.log('装饰器A工厂');
return function() { console.log('装饰器A应用'); };
}
function DecoratorB() {
console.log('装饰器B工厂');
return function() { console.log('装饰器B应用'); };
}
@DecoratorA()
@DecoratorB()
class ExampleClass {
@DecoratorB()
@DecoratorA()
method() {}
}
// 输出顺序:
// 装饰器B工厂
// 装饰器A工厂
// 装饰器A应用 (方法)
// 装饰器B应用 (方法)
// 装饰器B应用 (类)
// 装饰器A应用 (类)
4.2 装饰器组合模式
我们可以将多个装饰器组合起来,创建更复杂的行为:
typescript复制function Log() {
console.log('日志装饰器工厂');
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('应用日志装饰器');
};
}
function Cache() {
console.log('缓存装饰器工厂');
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('应用缓存装饰器');
};
}
function Transaction() {
console.log('事务装饰器工厂');
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('应用事务装饰器');
};
}
class DataService {
@Log()
@Cache()
@Transaction()
async fetchData(id: number) {
// 数据获取逻辑
}
}
// 输出顺序:
// 事务装饰器工厂
// 缓存装饰器工厂
// 日志装饰器工厂
// 应用日志装饰器
// 应用缓存装饰器
// 应用事务装饰器
这种组合模式在实际项目中非常有用,比如一个API方法可能需要同时具备日志记录、缓存和事务管理等功能。
5. 装饰器在实际项目中的应用
5.1 Vue组件装饰器
在Vue 3的Composition API中,我们可以使用装饰器简化代码:
typescript复制import { Options, Vue } from 'vue-class-component';
function LogLifecycle() {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`生命周期钩子 ${key} 被调用`);
return original.apply(this, args);
};
};
}
@Options({
props: {
msg: String
}
})
class MyComponent extends Vue {
count = 0;
@LogLifecycle()
mounted() {
console.log('组件已挂载');
}
increment() {
this.count++;
}
}
5.2 REST API控制器装饰器
在Node.js后端开发中,装饰器常用于定义API路由:
typescript复制import { Controller, Get, Post, Body } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll(): string {
return '返回所有用户';
}
@Post()
create(@Body() userData: any): string {
return `创建用户: ${JSON.stringify(userData)}`;
}
}
5.3 表单验证装饰器
结合class-validator库,我们可以创建强大的表单验证逻辑:
typescript复制import { validate, IsEmail, MinLength, MaxLength } from 'class-validator';
class UserRegistrationForm {
@IsEmail({}, { message: '必须是有效的邮箱地址' })
email: string;
@MinLength(8, { message: '密码至少8个字符' })
@MaxLength(20, { message: '密码最多20个字符' })
password: string;
}
const form = new UserRegistrationForm();
form.email = 'test@example.com';
form.password = '12345678';
validate(form).then(errors => {
if (errors.length > 0) {
console.log('验证错误:', errors);
} else {
console.log('验证通过');
}
});
6. 装饰器的高级技巧与最佳实践
6.1 元数据反射API
结合reflect-metadata库,装饰器可以实现更强大的元编程能力:
typescript复制import 'reflect-metadata';
function Column(type: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata('column:type', type, target, propertyKey);
};
}
class UserEntity {
@Column('varchar')
username: string;
@Column('int')
age: number;
}
function getColumnType(target: any, propertyKey: string): string {
return Reflect.getMetadata('column:type', target, propertyKey);
}
console.log(getColumnType(UserEntity.prototype, 'username')); // "varchar"
console.log(getColumnType(UserEntity.prototype, 'age')); // "int"
这种模式在ORM框架中非常常见,用于定义实体类与数据库表的映射关系。
6.2 装饰器性能优化
虽然装饰器很强大,但过度使用会影响性能。以下是一些优化建议:
- 避免在装饰器中进行复杂计算
- 对于高频调用的方法,考虑使用缓存
- 生产环境可以移除开发专用的装饰器(如详细日志)
typescript复制function ProductionOnly() {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (process.env.NODE_ENV !== 'production') {
descriptor.value = function() {
throw new Error('此方法仅在生产环境可用');
};
}
};
}
class ApiService {
@ProductionOnly()
criticalOperation() {
// 生产环境关键操作
}
}
6.3 装饰器与依赖注入
装饰器是实现依赖注入(DI)的理想选择:
typescript复制const serviceRegistry = new Map<string, any>();
function Injectable() {
return function(target: any) {
serviceRegistry.set(target.name, new target());
};
}
function Inject(serviceName: string) {
return function(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: () => serviceRegistry.get(serviceName)
});
};
}
@Injectable()
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class App {
@Inject('LoggerService')
logger: LoggerService;
run() {
this.logger.log('应用启动');
}
}
new App().run(); // 输出: [LOG] 应用启动
7. 常见问题与解决方案
7.1 装饰器不生效的排查步骤
- 确认tsconfig.json中启用了experimentalDecorators
- 检查装饰器是否应用在正确的目标上(类/方法/属性)
- 确保装饰器函数签名正确(参数数量和类型)
- 对于属性装饰器,注意它不能直接修改属性值
7.2 装饰器中的this问题
在方法装饰器中重写方法时,必须使用function而非箭头函数,以保持正确的this绑定:
typescript复制function BadDecorator() {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
// 错误:箭头函数会改变this绑定
descriptor.value = (...args: any[]) => original(...args);
};
}
function GoodDecorator() {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
// 正确:使用function保持this
descriptor.value = function(...args: any[]) {
return original.apply(this, args);
};
};
}
7.3 装饰器与继承
装饰器在继承链中的行为需要注意:
typescript复制function LogClass() {
return function(target: any) {
console.log(`装饰类: ${target.name}`);
};
}
@LogClass()
class Parent {
@LogMethod()
method() {}
}
class Child extends Parent {
@LogMethod()
method() {}
}
function LogMethod() {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(`装饰方法: ${propertyKey} in ${target.constructor.name}`);
};
}
// 输出顺序:
// 装饰方法: method in Parent
// 装饰类: Parent
// 装饰方法: method in Child
7.4 装饰器与类型安全
为了保持TypeScript的类型安全,可以为装饰器添加类型注解:
typescript复制interface MethodDecorator {
<T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
): TypedPropertyDescriptor<T> | void;
}
function TypeSafeDecorator(): MethodDecorator {
return function(target, propertyKey, descriptor) {
// 实现逻辑
return descriptor;
};
}
class Example {
@TypeSafeDecorator()
calculate(a: number, b: number): number {
return a + b;
}
}
8. 装饰器在大型项目中的架构应用
8.1 横切关注点分离
装饰器特别适合处理横切关注点(Cross-Cutting Concerns),如日志、缓存、权限等:
typescript复制// 权限检查装饰器
function RequireRole(role: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
const user = getUserFromContext();
if (!user.roles.includes(role)) {
throw new Error(`需要${role}权限`);
}
return original.apply(this, args);
};
};
}
class AdminController {
@RequireRole('admin')
deleteUser(userId: string) {
// 管理员操作
}
}
8.2 请求响应处理
在Web框架中,装饰器可以简化请求处理:
typescript复制function GET(path: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerRoute('GET', path, target[propertyKey]);
};
}
function POST(path: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerRoute('POST', path, target[propertyKey]);
};
}
class UserController {
@GET('/users')
getUsers() {
return userService.findAll();
}
@POST('/users')
createUser(@Body() userData: UserDTO) {
return userService.create(userData);
}
}
8.3 性能监控
装饰器可以无缝集成性能监控:
typescript复制function TrackPerformance(metricName: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
const start = Date.now();
try {
const result = await original.apply(this, args);
reportMetric(metricName, Date.now() - start, 'success');
return result;
} catch (error) {
reportMetric(metricName, Date.now() - start, 'error');
throw error;
}
};
};
}
class DataService {
@TrackPerformance('fetchData')
async fetchData(query: string) {
// 数据获取逻辑
}
}
9. 装饰器的未来与替代方案
9.1 ECMAScript装饰器提案进展
最新的装饰器提案与TypeScript当前实现有较大差异。主要变化包括:
- 更简洁的语法
- 更好的元数据支持
- 更一致的执行模型
javascript复制// 新提案示例(可能在未来TypeScript版本中实现)
@annotation
class MyClass {
@annotation
method() {}
@annotation
field;
}
9.2 高阶组件模式
在React等框架中,高阶组件(HOC)可以视为装饰器的一种替代方案:
typescript复制function withLogging(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log('组件已挂载');
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
@withLogging
class MyComponent extends React.Component {
render() {
return <div>装饰器模式</div>;
}
}
9.3 组合函数替代方案
对于简单的功能增强,有时组合函数比装饰器更直接:
typescript复制function withLogging(fn) {
return function(...args) {
console.log('函数调用开始');
const result = fn(...args);
console.log('函数调用结束');
return result;
};
}
const myFunction = withLogging(function(a, b) {
return a + b;
});
10. 装饰器实战:构建自定义ORM
让我们用一个完整的例子展示如何用装饰器构建简单的ORM系统:
typescript复制import 'reflect-metadata';
// 实体装饰器
function Entity(tableName: string) {
return function(target: any) {
Reflect.defineMetadata('entity:table', tableName, target);
};
}
// 列装饰器
function Column(options: { type: string; primary?: boolean }) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata('column:config', options, target, propertyKey);
};
}
// 简单的Repository实现
class Repository<T> {
constructor(private entity: new () => T) {}
save(instance: T): string {
const table = Reflect.getMetadata('entity:table', this.entity);
const columns: { [key: string]: any } = {};
for (const key of Object.keys(instance)) {
const options = Reflect.getMetadata('column:config', instance, key);
if (options) {
columns[key] = (instance as any)[key];
}
}
console.log(`保存到表 ${table}:`, columns);
return 'id-' + Math.random().toString(36).substr(2, 9);
}
}
// 使用示例
@Entity('users')
class User {
@Column({ type: 'string', primary: true })
id: string;
@Column({ type: 'string' })
name: string;
@Column({ type: 'number' })
age: number;
constructor(id: string, name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
}
const userRepo = new Repository(User);
const user = new User('123', '张三', 25);
userRepo.save(user); // 输出保存操作
这个简单的ORM展示了装饰器在元编程中的强大能力。通过定义@Entity和@Column装饰器,我们能够将类映射到数据库表,属性映射到列,而无需修改类本身的实现逻辑。
装饰器模式在TypeScript中提供了一种强大而灵活的方式来扩展和修改类及其成员的行为。从简单的日志记录到复杂的元编程,装饰器都能优雅地解决问题。随着ECMAScript装饰器提案的进展,这一特性将在未来的JavaScript生态中扮演更加重要的角色。