Appearance
TypeScript 中的装饰器:是什么、解决什么问题与实现示例
很多人第一次接触装饰器时,通常都会先记住一件事:它长得像给类、方法、属性前面贴了一个 @ 标记。
但如果只停在这个印象上,后面一遇到框架代码、NestJS、元数据、依赖注入、权限控制这些词,就很容易越看越乱。
装饰器真正要回答的是:能不能用一种更声明式的方式,把附加行为挂到类或类成员上。
1. 装饰器到底是什么
装饰器可以看成 一类在定义阶段附加额外逻辑或元信息的语法机制。
这里最值得抓住的词有两个:
定义阶段附加
也就是说,装饰器并不是“调用时突然魔法生效”的黑盒,它更像是在类、方法、字段这些结构刚被定义出来时,先插入一层加工逻辑。
最典型的长相就是:
ts
@sealed
class UserService {}或者:
ts
class UserService {
@log
findUser() {}
}所以装饰器可以粗略记成一句话:它是在类和类成员定义时,额外挂上一层行为或说明信息。
2. 装饰器在解决什么问题
如果没有装饰器,很多“横切逻辑”通常只能这样处理:
- 手动包一层函数
- 在每个方法里重复写日志
- 用大量配置对象去描述类和路由关系
- 用额外注册代码把元信息塞到框架里
这会带来几个常见问题:
- 重复代码多
- 业务逻辑和附加逻辑混在一起
- 类定义和框架配置分离得太远
- 声明关系不直观
装饰器最想解决的,通常不是“少写几行代码”,而是:把那些和业务主逻辑平行存在的附加规则,以更靠近定义位置的方式表达出来。
3. 可以把装饰器理解成“声明式扩展点”
这是最稳的一种入门理解方式。
例如你想表达:
- 这个类应该被冻结
- 这个方法调用时要打印日志
- 这个接口方法需要鉴权
- 这个属性需要校验
- 这个类应该注册到某个容器里
如果全都写成外部注册代码,通常会越来越散。
而装饰器的价值就在于:把这些规则直接贴回到类或成员定义旁边。
4. 装饰器常见能作用在哪些位置
不同写法和不同阶段的装饰器细节会有差异,但从使用视角看,最常见的关注点通常是:
- 类装饰器
- 方法装饰器
- 属性或字段装饰器
- 访问器装饰器
工程上最常见、最容易建立直觉的,通常还是前两个:
- 类装饰器:给一个类整体附加能力或元信息
- 方法装饰器:给一个方法加日志、权限、缓存、埋点一类逻辑
5. 一个最容易理解的示例:给方法加日志
先不要一上来就看复杂框架,可以看一个最简单的目标:希望某个方法每次执行前后都自动打印日志。
5.1 一种新版 decorators 风格的简化示例
ts
function log(value: Function, context: ClassMethodDecoratorContext) {
return function (this: unknown, ...args: unknown[]) {
console.log(`start: ${String(context.name)}`, args)
const result = value.apply(this, args)
console.log(`end: ${String(context.name)}`, result)
return result
}
}
class UserService {
@log
findUser(id: number) {
return { id, name: 'Tom' }
}
}这段代码最值得先理解的不是签名细节,而是执行思路:
@log先拿到原始方法- 它返回一个包装后的新方法
- 后续真正执行时,走的是这个包装后的版本
也就是说:装饰器最常见的一种能力,就是在不改业务调用方式的前提下,替你包一层增强逻辑。
5.2 这类写法到底解决了什么
如果不用装饰器,你通常就得:
- 手动改方法体
- 每个方法重复写日志
- 或者在类外再做一层额外包装
而装饰器把这个增强点放回了定义位置本身。
6. 一个类装饰器示例:给类附加冻结行为
类装饰器更适合拿来建立“作用在整个类上”的理解。
ts
function sealed<T extends new (...args: any[]) => any>(target: T, context: ClassDecoratorContext) {
Object.seal(target)
Object.seal(target.prototype)
}
@sealed
class ConfigService {
getEnv() {
return 'dev'
}
}可以直接把它理解成:
- 类刚定义出来时,装饰器就有机会介入
- 它可以给类本体或原型加附加处理
这个例子本身不是说“业务里一定要这么写”,而是帮助你建立一个稳定认识:类装饰器作用的是类定义本身,而不是某一次具体调用。
7. 装饰器最常见的应用场景
装饰器真正高频出现的地方,通常不是语言教学 demo,而是框架和工程约定。
7.1 元信息声明
例如声明:
- 这个类属于哪个模块
- 这个方法对应哪个路由
- 这个属性需要什么校验规则
这类场景里,装饰器更多是在做:声明信息贴附。
7.2 依赖注入
很多后端或工程框架会用装饰器表达:
- 哪个类应该注册进容器
- 哪个依赖需要被注入
这类场景里,装饰器更像:容器和类定义之间的桥。
7.3 日志、鉴权、缓存、埋点
这类都属于很典型的横切逻辑。
它们的共同特点是:
- 不只属于某一个业务方法
- 会在很多地方重复出现
- 直接混进业务代码后容易让方法体变脏
所以装饰器非常适合表达:
- 方法执行前后记录日志
- 某些方法需要权限检查
- 某些调用结果可以缓存
- 某些方法需要埋点统计
8. 为什么装饰器经常和框架一起出现
因为框架特别需要“声明式注册”。
例如框架很想知道:
- 哪些类是控制器
- 哪些方法是路由处理函数
- 哪些参数需要校验
- 哪些类要交给依赖注入容器管理
如果没有装饰器,通常就需要:
- 单独写很多配置
- 写大量注册代码
- 手动把类、方法和框架行为再绑定一次
所以从框架视角看,装饰器真正有吸引力的地方在于:它让“结构定义”和“框架约定”更容易贴在一起。
9. 装饰器和高阶函数、代理模式有什么关系
这几个概念看起来有点像,但不要混在一起。
9.1 和高阶函数的关系
方法装饰器很多时候看起来像在做:
- 接收一个函数
- 返回一个增强后的函数
这和高阶函数的思路确实很接近。
所以可以直接把它理解成:方法装饰器经常会借助高阶函数式的包装思路实现增强。
9.2 和代理模式的关系
代理模式更偏运行时对象层面的转发和控制。
而装饰器更偏:在定义阶段把增强逻辑贴上去。
所以它们可能都能实现“增强”,但时机和语义不一样。
10. 为什么现在项目里的装饰器写法可能不一样
这是最容易把人看晕的一点。
很多人会发现:
- 有的项目里装饰器函数签名是旧样子
- 有的项目里是新版 decorators 风格
- 有的项目还会搭配
emitDecoratorMetadata
这不是你看错了,而是因为:TypeScript 生态里长期同时存在“历史项目写法”和“较新的 decorators 语义”。
11. 新版 decorators 和历史项目写法为什么会并存
可以直接把它理解成:
- 较新的 TypeScript 已支持新版 decorators 语义
- 但大量历史项目和部分框架生态,仍然在使用更早期的装饰器模式
这会带来一个直接结果:同样写 @xxx,不同项目里的底层行为和函数签名可能并不完全一样。
所以工程上要注意的是:
- 当前项目到底采用的是哪一套写法
- 当前框架是否依赖历史装饰器生态
- 是否还需要配合元数据能力一起使用
12. experimentalDecorators 和 emitDecoratorMetadata 是什么
这两个配置经常一起出现,但它们不是一个东西。
12.1 experimentalDecorators
它通常和历史装饰器写法强相关。
可以看成 告诉 TypeScript,当前项目要启用那套更早期的装饰器支持方式。
12.2 emitDecoratorMetadata
它更偏:在编译产物里额外发出一部分和类型相关的元数据信息。
这类能力常见于依赖注入、参数推断、框架反射一类场景。
但要注意:它不是“有了装饰器就一定必须开”的通用选项,而是看你的框架和运行机制是否真的需要这些元信息。
13. 一个更贴近框架思维的示例
下面这个例子不追求可直接放进生产,而是帮助你理解“元信息贴附”这件事。
ts
const routes = new Map<string, string>()
function Get(path: string) {
return function (_value: Function, context: ClassMethodDecoratorContext) {
routes.set(String(context.name), path)
}
}
class UserController {
@Get('/users')
list() {
return ['Tom', 'Lucy']
}
}
console.log(routes.get('list')) // /users这段示例真正要表达的是:
- 业务方法本身仍然写自己的业务逻辑
- 装饰器额外给这个方法贴上“路由路径”这类说明信息
- 框架后续就可以基于这些元信息完成注册
这也是为什么很多框架喜欢装饰器:它很适合把声明信息贴回类和方法定义本身。
14. 装饰器的优点是什么
如果装饰器用得合适,它最常见的优点包括:
- 声明更集中
- 横切逻辑更容易抽离
- 类和框架约定靠得更近
- 某些重复增强逻辑更容易复用
尤其在下面这些场景里,它会显得很顺手:
- 路由声明
- 参数校验
- 权限控制
- 依赖注入
- 日志和缓存增强
15. 装饰器的代价和边界是什么
这部分同样重要,不能只讲它“很优雅”。
15.1 可读性未必总是更强
如果装饰器叠得太多,读代码的人往往会很难第一眼看清:
- 方法真正被增强了什么
- 执行顺序是什么
- 哪些行为来自业务本体,哪些来自装饰器
15.2 调试成本可能上升
因为你看到的类定义,不一定就是最终真正执行时的完整行为。
15.3 框架绑定更重
有些项目大量依赖框架装饰器后,会让类定义和框架本身高度耦合。
15.4 不适合把所有逻辑都装饰器化
装饰器适合的是:横切逻辑和声明式规则。
它不适合替代正常的模块设计、函数抽象和业务建模。
16. 工程里最容易踩的坑
16.1 误把装饰器当成“更高级的语法糖”
它不只是语法写法变化,而是会影响定义阶段行为和框架接入方式。
16.2 不区分新版 decorators 和历史项目写法
这会直接导致:
- demo 能看懂
- 一进真实项目就发现签名对不上
16.3 过度使用装饰器
结果就是:
- 类定义上贴满了
@xxx - 真正执行链路越来越不直观
16.4 把装饰器当成万能抽象
它能解决的是一类问题,不是所有重复逻辑都适合用它处理。
17. 一份检查清单
当你考虑要不要用装饰器时,至少可以问自己:
- 这段逻辑是不是横切逻辑
- 这部分信息是不是更适合声明式表达
- 当前项目采用的是新版 decorators,还是历史项目写法
- 当前框架是否真的需要元数据支持
- 用装饰器之后,团队成员会不会更容易理解,而不是更难
18. 小结
这一篇最想建立的核心认识是:
- 装饰器是在定义阶段给类或类成员附加行为或元信息的机制
- 它真正擅长的是声明式规则和横切逻辑
- 框架场景里它常被用于路由、依赖注入、校验、权限和日志增强
- 工程里一定要区分新版 decorators 和历史装饰器写法
- 装饰器有价值,但也有可读性和调试成本
如果只压缩成一句话,可以记住:
装饰器不是为了让代码看起来更花哨,而是为了把附加规则和增强逻辑,更靠近类与成员的定义位置表达出来。