装饰器(Decorators) 是 TypeScript 中一个强大且优雅的特性,它允许你通过声明式语法在类、方法或属性级别添加额外功能。作为 ECMAScript 提案的一部分,TypeScript 率先实现了这一特性,使其成为实现面向切面编程(AOP)的理想工具。
为什么需要装饰器?
在软件开发中,我们经常遇到一些横切关注点(Cross-Cutting Concerns):
- 日志记录
- 身份验证
- 验证
- 缓存
- 性能监控
传统OOP方法会导致代码重复和核心逻辑的污染。装饰器通过提供一种声明式的方式来解决这些问题:
// 原始类
class UserService {
getUser(id: number) {
// 业务逻辑
}
}
// 使用装饰器增强
class UserService {
@LogExecutionTime
@CacheResult
getUser(id: number) {
// 核心业务逻辑保持简洁
}
}
启用装饰器支持
装饰器目前是一个实验性功能,需要在 tsconfig.json 中启用:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
还需安装Babel插件(若使用Babel):
npm install --save-dev @babel/plugin-proposal-decorators
装饰器类型与基本语法
TypeScript支持五种主要装饰器类型:
1. 类装饰器
应用于类构造函数,用于修改或替换类定义
function LogClass(target: Function) {
console.log(`类装饰器应用于: ${target.name}`);
// 添加元数据
Reflect.defineMetadata('createdAt', new Date(), target);
}
@LogClass
class ApiService {
// 类实现
}
类装饰器工厂
通过工厂函数向装饰器传递参数:
function LogClass(prefix: string) {
return function(target: Function) {
console.log(`${prefix}: ${target.name} 类已装饰`);
// 添加类元数据
Reflect.defineMetadata('prefix', prefix, target);
}
}
@LogClass('API')
class ApiService {}
2. 方法装饰器
应用于类的方法,用于修改方法行为
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`调用方法: ${propertyKey},参数: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`方法返回: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
class UserService {
@LogMethod
getUser(id: number) {
return { id, name: 'John Doe' };
}
}
3. 访问器装饰器
应用于类属性的 getter/setter
function ReadOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
return descriptor;
}
class User {
private _name: string;
constructor(name: string) {
this._name = name;
}
@ReadOnly
get name() {
return this._name;
}
}
const user = new User('Alice');
user.name = 'Bob'; // 错误: 无法分配到只读属性
4. 属性装饰器
应用于类属性
function DefaultValue(value: any) {
return function(target: any, propertyKey: string) {
// 设置属性默认值
if (!target[propertyKey]) {
target[propertyKey] = value;
}
// 添加元数据
Reflect.defineMetadata('defaultValue', value, target, propertyKey);
}
}
class Settings {
@DefaultValue('light')
theme: string;
@DefaultValue(10)
itemsPerPage: number;
}
const settings = new Settings();
console.log(settings.theme); // "light"
console.log(settings.itemsPerPage); // 10
5. 参数装饰器
应用于类方法参数
function ValidateParam(type: 'number' | 'string' | 'boolean') {
return function(target: any, methodName: string, parameterIndex: number) {
// 注册验证元数据
const existingValidations = Reflect.getMetadata('validations', target, methodName) || [];
existingValidations.push({
index: parameterIndex,
type
});
Reflect.defineMetadata('validations', existingValidations, target, methodName);
}
}
装饰器执行顺序
理解装饰器的执行顺序至关重要:
function DecoratorA() {
console.log('A 工厂');
return (target: any) => console.log('A 装饰器');
}
function DecoratorB() {
console.log('B 工厂');
return (target: any) => console.log('B 装饰器');
}
@DecoratorA()
@DecoratorB()
class Example {
@DecoratorA()
@DecoratorB()
method() {}
}
执行结果为:
A 工厂
B 工厂
B 装饰器
A 装饰器
A 工厂
B 工厂
B 装饰器
A 装饰器
执行规则:
- 从上到下执行类装饰器工厂
- 从下到上应用类装饰器
- 对于每个成员:
- 从上到下执行方法/属性装饰器工厂
- 从下到上应用方法/属性装饰器
元数据反射 API
结合 reflect-metadata 库可以更强大地使用装饰器:
npm install reflect-metadata
在应用入口导入:
import "reflect-metadata";
使用元数据示例
// 定义类型验证装饰器
function ValidateTypes(target: any, methodName: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const validations = Reflect.getMetadata('validations', target, methodName) || [];
descriptor.value = function(...args: any[]) {
// 参数验证
validations.forEach(validation => {
const paramValue = args[validation.index];
const expectedType = validation.type;
if (typeof paramValue !== expectedType) {
throw new Error(
`参数 #${validation.index} 应为 ${expectedType} 类型, 实际为 ${typeof paramValue}`
);
}
});
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@ValidateTypes
add(
@ValidateParam('number') a: number,
@ValidateParam('number') b: number
) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3); // 5
calc.add(2, '3'); // 错误: 参数 #1 应为 number 类型, 实际为 string
装饰器实战应用
1. API 路由装饰器(Express/Koa)
const routes: any[] = [];
function Controller(prefix: string = '') {
return function(target: any) {
// 存储控制器元数据
Reflect.defineMetadata('prefix', prefix, target);
if (!Reflect.hasMetadata('routes', target)) {
Reflect.defineMetadata('routes', [], target);
}
};
}
function Get(path: string) {
return function(target: any, propertyKey: string) {
const routes: any[] = Reflect.getMetadata('routes', target.constructor) || [];
routes.push({
method: 'get',
path,
handlerName: propertyKey
});
Reflect.defineMetadata('routes', routes, target.constructor);
};
}
function Post(path: string) {
return function(target: any, propertyKey: string) {
// 类似Get装饰器实现...
};
}
// 使用示例
@Controller('/users')
class UserController {
@Get('/')
getAllUsers() {
return { users: [] };
}
@Get('/:id')
getUserById(id: string) {
return { id, name: 'Alice' };
}
@Post('/')
createUser() {
// 创建用户
}
}
// 注册路由的函数
function registerControllers(app: any, controllers: any[]) {
controllers.forEach(Controller => {
const prefix = Reflect.getMetadata('prefix', Controller);
const instance = new Controller();
const routes: any[] = Reflect.getMetadata('routes', Controller) || [];
routes.forEach(route => {
app[route.method](
`${prefix}${route.path}`,
instance[route.handlerName].bind(instance)
);
console.log(`注册路由: [${route.method.toUpperCase()}] ${prefix}${route.path}`);
});
});
}
// Express应用中使用(伪代码)
import express from 'express';
const app = express();
registerControllers(app, [UserController]);
app.listen(3000);
2. 依赖注入系统
const dependencies = new Map();
function Injectable(key: string) {
return function(target: any) {
dependencies.set(key, target);
};
}
function Inject(key: string) {
return function(target: any, propertyKey: string) {
// 等待类实例化时解析依赖
Object.defineProperty(target, propertyKey, {
get: () => dependencies.get(key),
enumerable: true,
configurable: true
});
};
}
// 使用示例
@Injectable('UserRepository')
class UserRepository {
getUsers() {
return [{ id: 1, name: 'Alice' }];
}
}
@Injectable('UserService')
class UserService {
@Inject('UserRepository')
userRepository!: UserRepository;
getUsers() {
return this.userRepository.getUsers();
}
}
@Injectable('AppController')
class AppController {
@Inject('UserService')
userService!: UserService;
run() {
const users = this.userService.getUsers();
console.log('Users:', users);
}
}
// 入口点
const main = () => {
const controller = dependencies.get('AppController');
controller.run();
};
main(); // 输出 Users: [ { id: 1, name: 'Alice' } ]
3. ORM 实体模型
import "reflect-metadata";
const ENTITY_METADATA_KEY = Symbol('entity:fields');
function Entity(tableName: string) {
return function(target: any) {
Reflect.defineMetadata('table', tableName, target);
};
}
function Column(options: { type: string, primary?: boolean, nullable?: boolean }) {
return function(target: any, propertyKey: string) {
const fields = Reflect.getMetadata(ENTITY_METADATA_KEY, target) || [];
fields.push({
name: propertyKey,
type: options.type,
primary: options.primary || false,
nullable: options.nullable || false
});
Reflect.defineMetadata(ENTITY_METADATA_KEY, fields, target);
};
}
// 使用示例
@Entity('users')
class User {
@Column({ type: 'number', primary: true })
id: number;
@Column({ type: 'string' })
name: string;
@Column({ type: 'string', nullable: true })
email?: string;
constructor(id: number, name: string, email?: string) {
this.id = id;
this.name = name;
this.email = email;
}
}
// ORM工具函数
function save(entity: any) {
const table = Reflect.getMetadata('table', entity.constructor);
const fields = Reflect.getMetadata(ENTITY_METADATA_KEY, entity) || [];
const columns = [];
const values = [];
fields.forEach(field => {
const value = entity[field.name];
if (value !== undefined || field.nullable) {
columns.push(field.name);
values.push(value);
} else if (!field.nullable) {
throw new Error(`字段 ${field.name} 不能为空`);
}
});
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map((_, i) => '?').join(', ')})`;
console.log('执行SQL:', sql);
console.log('参数:', values);
// 实际执行数据库操作...
}
// 使用ORM
const user = new User(1, 'Alice', 'alice@example.com');
save(user);
4. 响应式属性装饰器(Vue/Angular风格)
function ReactiveProperty(target: any, propertyKey: string) {
const privateKey = `_${propertyKey}`;
// 替换属性定义
Object.defineProperty(target, propertyKey, {
get: function() {
return this[privateKey];
},
set: function(newValue) {
if (this[privateKey] !== newValue) {
const oldValue = this[privateKey];
this[privateKey] = newValue;
// 通知变化
if (this.propertyChanged) {
this.propertyChanged(propertyKey, oldValue, newValue);
}
}
},
enumerable: true,
configurable: true
});
}
// 使用示例
class ViewModel {
// 变化通知事件
propertyChanged?: (property: string, oldValue: any, newValue: any) => void;
@ReactiveProperty
firstName = 'John';
@ReactiveProperty
lastName = 'Doe';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
constructor() {
// 监听属性变化
this.propertyChanged = (prop, oldVal, newVal) => {
console.log(`属性 "${prop}" 已更改: ${oldVal} -> ${newVal}`);
if (prop === 'firstName' || prop === 'lastName') {
console.log('全名更新为:', this.fullName);
}
};
}
}
const vm = new ViewModel();
vm.firstName = 'Alice';
// 输出:
// 属性 "firstName" 已更改: John -> Alice
// 全名更新为: Alice Doe
vm.lastName = 'Smith';
// 输出:
// 属性 "lastName" 已更改: Doe -> Smith
// 全名更新为: Alice Smith
装饰器最佳实践
1. 保持装饰器职责单一
不良实践:
function LogAndValidate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 同时处理日志和验证 - 违反单一职责原则
}
良好实践:
@LogExecutionTime
@ValidateParameters
@CacheResult(60)
async getUser(id: number) {
// ...
}
2. 避免副作用,保持幂等性
装饰器应该是幂等的:
// 良好实践:幂等装饰器
function Decorate(target: any) {
if (!target.__decorated) {
// 应用装饰逻辑
target.__decorated = true;
}
}
3. 提供清晰的自定义选项
使用配置对象而不是多个参数:
// 更易读的配置
function Cache(options: { ttl: number; key?: string; }) {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
// 实现
};
}
4. 考虑继承和装饰器链
正确处理装饰器在继承链中的行为:
class BaseClass {
@DecoratorA
method() {}
}
class ChildClass extends BaseClass {
@DecoratorB
method() {}
}
// DecoratorA 和 DecoratorB 都会应用到 ChildClass 的 method
5. 谨慎使用元数据反射
避免过度使用反射以减少性能开销:
// 在可能的情况下缓存反射结果
const cachedMetadata = new Map();
function getMetadata(target: any) {
if (!cachedMetadata.has(target)) {
cachedMetadata.set(target, Reflect.getMetadata(...));
}
return cachedMetadata.get(target);
}
TypeScript 装饰器的未来
虽然装饰器目前处于Stage 2提案阶段,但TypeScript团队正积极与TC39合作推进标准。当前实现与未来标准的主要差异:
| 特性 | 当前TS实现 | ECMAScript提案 |
|---|---|---|
| 语法 | @decorator | 相同 |
| 类装饰器目标 | 构造函数 | 构造函数 |
| 参数顺序 | 目标、键、描述符 | 值、上下文对象 |
| 上下文信息 | 有限的 | 提供完整上下文对象 |
迁移策略:
// 当前TypeScript风格
function legacyDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// ...
}
// 未来兼容风格
function standardDecorator(value: any, context: DecoratorContext) {
if (context.kind === 'method') {
// 处理类方法
}
}
元编程的艺术
装饰器为TypeScript开发提供了强大的元编程能力,使开发者能够:
- 🧩 优雅地实现横切关注点分离
- 🚀 实现声明式编程范式
- 🔗 构建复杂框架(Angular、NestJS)
- 📦 创建可重用功能组件
- ⚙️ 增强元编程能力(依赖注入、AOP)
关键知识点回顾:
- 五种装饰器类型:类、方法、访问器、属性、参数
- 装饰器工厂模式实现参数化
- 反射元数据API的集成使用
- 装饰器执行顺序规则
- 真实世界应用:Web框架、ORM、DI、响应式编程
正如设计模式大师Robert C. Martin所说:"装饰器模式允许向对象动态添加行为而不改变对象本身。" TypeScript装饰器将这一理念提升到语言级别,为现代应用开发提供了强大的抽象能力。
掌握装饰器,你将能够:
- 开发企业级框架和库
- 编写更清晰、更模块化的代码
- 实现复杂的横切关注点处理
- 构建声明式API系统
- 提升架构设计能力