装饰器(Decorators)
什么是装饰器?
装饰器是一种特殊的声明,可以附加到类、方法、访问器、属性或参数上,用于修改或增强它们的行为。它的本质是一个函数,在类定义被求值时被调用,接收目标元素作为参数,可以选择返回修改后的版本或读取/写入元数据。
装饰器实现了 AOP(面向切面编程)模式:将日志记录、权限检查、参数验证等横切关注点(cross-cutting concerns)从业务逻辑中分离出来,以声明式的方式附加到代码上,而不需要修改业务代码本身。
装饰器的两个时代
TypeScript 有两套装饰器实现,选择哪套取决于你用的框架:
旧版装饰器(experimentalDecorators: true)
基于 TC39 Stage 1 提案(约 2016 年)。NestJS、TypeORM、Angular 等框架广泛使用,生态成熟。需要同时开启 experimentalDecorators 和 emitDecoratorMetadata(用于 reflect-metadata)。与 ECMAScript 最终标准不兼容,未来会逐步被新版取代。
新版装饰器(TypeScript 5.0+,Stage 3)
基于 TC39 Stage 3 提案,是 ECMAScript 的未来标准。TypeScript 5.0 起默认支持,无需配置。API 与旧版完全不同(Context 对象替代了直接参数),不支持 reflect-metadata(元数据 API 有独立 Stage 3 提案)。当前主流框架尚未迁移,新项目建议观望。
新版装饰器(Stage 3,TS 5.0+)
// 类装饰器
function sealed(target: Function, ctx: ClassDecoratorContext) {
Object.seal(target);
Object.seal(target.prototype);
console.log(`Sealed class: ${String(ctx.name)}`);
}
@sealed
class BugReport {
type = 'report';
title: string;
constructor(t: string) { this.title = t; }
}
// 方法装饰器
function logCall(target: Function, ctx: ClassMethodDecoratorContext) {
return function (this: unknown, ...args: unknown[]) {
console.log(`调用方法: ${String(ctx.name)}, 参数:`, args);
return target.apply(this, args);
};
}
class Calculator {
@logCall
add(a: number, b: number) { return a + b; }
}
旧版装饰器与 NestJS
// tsconfig.json(NestJS 项目)
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
// NestJS 控制器示例(旧版装饰器)
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}
装饰器工厂模式
// 装饰器工厂:返回装饰器的函数,允许传参
function MinLength(min: number) {
return function (target: unknown, ctx: ClassFieldDecoratorContext) {
return function (this: unknown, initialValue: string) {
if (initialValue.length < min) {
throw new Error(`字段 ${String(ctx.name)} 最少 ${min} 个字符`);
}
return initialValue;
};
};
}
class User {
@MinLength(3)
name: string;
constructor(name: string) {
this.name = name; // 会触发装饰器验证
}
}
new User('张三'); // ✅
new User('张'); // ❌ Error: 字段 name 最少 3 个字符
reflect-metadata 与元数据
旧版装饰器配合 reflect-metadata 库,可以在运行时读写附加在类/方法/属性上的元数据。emitDecoratorMetadata: true 让 TypeScript 在编译时自动将类型信息(参数类型、返回类型)作为元数据写入,这正是 NestJS 依赖注入的工作原理。
import 'reflect-metadata'; // 必须最先引入
// 自定义元数据键(避免命名冲突)
const ROLES_KEY = 'roles';
// 旧版装饰器:权限控制
function Roles(...roles: string[]) {
// 将 roles 数组作为元数据附加到方法上
return Reflect.metadata(ROLES_KEY, roles);
}
class AppController {
@Roles('admin')
deleteUser(id: number) { /* ... */ }
@Roles('user', 'admin')
getProfile() { /* ... */ }
}
// 运行时读取元数据(用于权限检查中间件)
function checkAccess(controller: object, methodName: string, userRole: string) {
const requiredRoles: string[] = Reflect.getMetadata(ROLES_KEY, controller, methodName) ?? [];
return requiredRoles.length === 0 || requiredRoles.includes(userRole);
}
const ctrl = new AppController();
console.log(checkAccess(ctrl, 'deleteUser', 'user')); // false
console.log(checkAccess(ctrl, 'getProfile', 'user')); // true
新版装饰器的五种类型
ClassDecoratorContext
类装饰器的上下文对象,包含 kind: 'class'、name(类名)、addInitializer()(添加初始化器)。类装饰器在类定义被求值后立即执行,可以返回一个新的类替代原类(类似 Proxy),也可以只读取/修改 prototype。
ClassMethodDecoratorContext
方法装饰器的上下文,包含 kind: 'method'、name、static(是否静态)、private(是否私有)、access(读写访问器)。返回的函数会替换原方法。
ClassFieldDecoratorContext
字段装饰器的上下文,包含 kind: 'field'、name、static、private。返回一个初始化函数(接受初始值,返回新初始值),在实例创建时调用。用于字段级别的验证和转换。
ClassAccessorDecoratorContext
访问器装饰器(auto accessor)的上下文,包含 kind: 'accessor'。可以重写 get 和 set 行为,是新版 Stage 3 装饰器对旧版属性装饰器的替代方案。
ClassGetterDecoratorContext / ClassSetterDecoratorContext
getter/setter 装饰器的上下文,kind 分别是 'getter' 和 'setter'。可以拦截属性的读写,实现惰性初始化、验证、日志等逻辑。
// 新版装饰器完整示例:带记忆化(memoize)的方法装饰器
function memoize<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
): (this: This, ...args: Args) => Return {
// 在每个实例上维护独立的缓存(WeakMap 防止内存泄漏)
const cache = new WeakMap<object, Map<string, Return>>();
return function (this: This, ...args: Args): Return {
// 用 this 作为实例键(每个实例有自己的缓存)
if (!cache.has(this as object)) {
cache.set(this as object, new Map());
}
const instanceCache = cache.get(this as object)!;
// 用序列化的参数作为缓存键
const key = JSON.stringify(args);
if (instanceCache.has(key)) {
console.log(`[${String(ctx.name)}] 命中缓存`);
return instanceCache.get(key)!;
}
// 未命中:执行原始方法并缓存结果
const result = target.call(this, ...args);
instanceCache.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 svc = new MathService();
console.log(svc.fibonacci(40)); // 快速!后续相同参数直接命中缓存
TypeORM 装饰器示例
// TypeORM 实体:旧版装饰器将类映射为数据库表
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
@Entity() // 声明这个类对应数据库中的一张表
export class Post {
@PrimaryGeneratedColumn() // 自增主键
id: number;
@Column({ length: 200 }) // varchar(200) 列
title: string;
@Column('text') // text 类型列
content: string;
@Column({ default: false }) // 有默认值的列
published: boolean;
// ManyToOne:多篇文章属于同一作者
@ManyToOne(() => User, user => user.posts)
author: User;
}
生产项目建议
如果你的项目使用 NestJS 或 TypeORM,必须使用旧版装饰器(experimentalDecorators: true + emitDecoratorMetadata: true),这些框架还未迁移到新版标准。如果你在写新的独立代码,推荐等待主流框架完成迁移后再使用新版装饰器,两套 API 不互相兼容。
本章小结
本章核心要点
- 装饰器的本质:类定义被求值时自动调用的函数,实现 AOP(面向切面编程),将日志/权限/验证等横切逻辑从业务代码中分离,以声明式语法附加。
- 两套装饰器不互通:旧版(experimentalDecorators)被 NestJS/TypeORM/Angular 使用,生产稳定;新版(TS 5.0+)是 Stage 3 标准,API 完全不同,主流框架尚未迁移。
- 装饰器工厂模式:返回装饰器的函数(@MinLength(3)),允许传参定制行为;多个装饰器叠加时,执行顺序是从下到上(最接近目标的先执行)。
- reflect-metadata:旧版装饰器的伴侣,在运行时读写类型元数据;emitDecoratorMetadata: true 让编译器自动将参数类型信息注入元数据,这是 NestJS 依赖注入自动解析类型的基础。
- 使用场景:NestJS 路由/Guard/Pipe、TypeORM 实体映射、class-validator 参数验证、自定义日志和性能监控——这些场景中装饰器极大减少了样板代码。