阮一峰《TypeScript 教程》学习笔记——装饰器
1. 一段话总结
TypeScript 装饰器是定义时修改类(及成员)行为的语法结构,核心特征为**@前缀+函数表达式**(表达式需最终返回函数),接受value(被装饰对象)和context(上下文,含kind/name等关键属性)两个参数,要么无返回值要么返回新对象替代目标;TS 5.0 同时支持标准语法(直接使用)和传统语法(需开启--experimentalDecorators编译参数);包含类、方法、属性、getter、setter、accessor 六种类型,执行分评估阶段(计算@后表达式得装饰器函数)和应用阶段(按“方法→静态属性→实例属性→类”顺序应用,多装饰器内层先执行);核心用途是简洁扩展类能力(如添加方法、延迟执行、绑定this),但需注意不同装饰器的参数差异(如属性装饰器value恒为undefined)。
2. 思维导图

3. 详细总结
一、装饰器基础
-
定义与核心作用
装饰器(Decorator)是 TypeScript 中用于在定义阶段修改类及成员行为的语法结构,无需创建子类即可扩展类能力(如添加方法、改写逻辑),比子类继承更简洁。 -
语法特征
特征 说明 示例 前缀 必须以 @开头,后接表达式@log、@delay(1000)表达式要求 最终需返回函数(装饰器函数) @myFuncFactory(123)(工厂函数返回装饰器)参数 自动接收 value(被装饰对象)和context(上下文)function decorator(value: any, context: any) {}返回值 要么无返回值,要么返回新对象替代被装饰目标 方法装饰器返回新函数替代原方法 -
版本差异(TS 5.0)
TS 5.0 支持两种装饰器语法,核心区别如下:语法类型 启用方式 适用场景 标准语法 直接使用,无需编译参数 新项目,遵循ECMAScript标准 传统语法 需开启 --experimentalDecorators参数(如tsc --target ES5 --experimentalDecorators)维护旧TS项目
二、装饰器结构(核心:函数与参数)
装饰器本质是一个函数,类型定义如下,关键在于value和context两个参数:
type Decorator = (value: DecoratedValue, // 被装饰的对象(如类、方法)context: { // 上下文对象,描述被装饰对象的元信息kind: string; // 装饰类型(class/method/field等)name: string | symbol; // 被装饰对象的名称addInitializer?(initializer: () => void): void; // 添加初始化逻辑static?: boolean; // 是否为静态成员private?: boolean; // 是否为私有成员access: { get?(): unknown; set?(value: unknown): void }; // 存取器}
) => void | ReplacementValue; // 无返回值或返回新对象替代目标
- context 核心属性:
kind(必选,区分装饰类型)和name(必选,对象名称)是所有装饰器共有的,其他属性(如static/private)仅特定装饰器有(如方法装饰器含static)。 - addInitializer 作用:替代构造函数中的初始化逻辑(如绑定
this),在类初始化时执行。
三、六种装饰器类型详解
六种装饰器对应不同的类成员,核心差异体现在value参数、context.kind及作用场景,具体对比如下:
| 装饰器类型 | value 参数 | context.kind | 核心作用 | 典型场景 | 返回值 |
|---|---|---|---|---|---|
| 类装饰器 | 类本身(Function) | ‘class’ | 修改类原型、替换构造函数 | 实例计数、禁止new调用 | 无 / 新类 / 新构造函数 |
| 方法装饰器 | 方法本身(Function) | ‘method’ | 改写方法、添加日志、延迟执行 | 方法调用日志、this绑定 | 无 / 新方法 |
| 属性装饰器 | undefined(无意义) | ‘field’ | 修改属性初始值 | 初始化日志、值翻倍 | 无 / 初始化函数(参数为初始值,返回最终值) |
| getter 装饰器 | getter函数(Function) | ‘getter’ | 改写getter、缓存结果 | 缓存开销大的计算结果 | 无 / 新getter |
| setter 装饰器 | setter函数(Function) | ‘setter’ | 改写setter、添加值校验 | 限制属性赋值范围 | 无 / 新setter |
| accessor 装饰器 | 含get/set的对象 | ‘accessor’ | 改写自动生成的get/set | 为accessor添加日志 | 无 / 新get/set/init函数 |
各类型示例(关键场景)
-
类装饰器:实例计数
function countInstances(value: any) {let count = 0;return class extends value { // 返回新子类constructor(...args: any[]) {super(...args);count++;this.count = count;}}; } @countInstances class MyClass {} new MyClass().count; // 1 -
方法装饰器:延迟执行
function delay(ms: number) {return (value: any) => {return function (...args: any[]) {setTimeout(() => value.apply(this, args), ms);};}; } class Logger {@delay(1000) // 延迟1秒执行log(msg: string) { console.log(msg); } } -
属性装饰器:初始值翻倍
function twice() {return (initialValue: any) => initialValue * 2; // 返回初始化函数 } class C {@twice field = 3; // 最终值为6 }
四、装饰器执行顺序
装饰器执行分评估阶段和应用阶段,顺序严格且影响功能正确性:
-
评估阶段(Evaluation)
- 定义:计算
@后表达式的值,得到装饰器函数(本质是“获取装饰器”)。 - 顺序:按装饰器出现的先后顺序评估,类装饰器先于类内部装饰器;若成员名是计算值(如
[Symbol.iterator]()),则在对应装饰器评估后计算。 - 示例:
@d('类')→@d('静态属性')→@d('方法')(评估顺序)。
- 定义:计算
-
应用阶段(Application)
- 定义:将评估得到的装饰器函数应用于被装饰对象(本质是“执行装饰器”)。
- 顺序:
- 原型方法装饰器 → 2. 静态属性/方法装饰器 → 3. 实例属性装饰器 → 4. 类装饰器;
- 若一个成员有多个装饰器(如
@bound @log greet()),则内层装饰器先执行(@log先应用,@bound后应用于@log的结果)。
-
执行顺序示例输出
参考网页示例,执行顺序如下(关键步骤):评估 @d(): 类装饰器 → 评估 @d(): 静态属性装饰器 → 评估 @d(): 方法装饰器 → 计算方法名 → 评估 @d(): 实例属性装饰器 应用 @d(): 方法装饰器 → 应用 @d(): 静态属性装饰器 → 应用 @d(): 实例属性装饰器 → 应用 @d(): 类装饰器
4. 关键问题
问题1:TS 5.0 中装饰器的两种语法(标准 vs 传统)在启用方式、语法特征上有何核心差异?为何官方更推荐标准语法?
答案:
两者核心差异如下表,官方推荐标准语法的核心原因是其符合ECMAScript标准,兼容性更强且无需额外编译参数:
| 对比维度 | 标准语法 | 传统语法 |
|---|---|---|
| 启用方式 | 直接使用,无需编译参数 | 需开启--experimentalDecorators编译参数 |
| 语法特征 | 装饰器函数接受value和context参数,返回值规则明确 | 装饰器函数参数为target/propertyKey/descriptor(不同装饰器参数不同) |
| 兼容性 | 遵循ECMAScript标准,浏览器/Node.js未来可原生支持 | TS私有语法,无标准支持,仅TS环境可用 |
| 核心优势 | 类型安全(context参数有明确接口如ClassMethodDecoratorContext) | 历史兼容性(适配旧TS项目) |
官方推荐标准语法的原因:
- 符合ECMAScript语言发展方向,避免依赖TS私有语法导致的迁移成本;
context参数提供更丰富的元信息(如kind/addInitializer),支持更复杂的初始化逻辑;- 无需额外编译参数,降低项目配置复杂度。
问题2:属性装饰器的value参数为何恒为undefined?如何通过属性装饰器修改属性的初始值?
答案:
-
value为undefined的原因:
属性装饰器作用于“类顶部声明的属性(field)”,其执行时机是类定义阶段,此时属性尚未初始化(初始值在实例化时才赋值),因此无法通过value获取属性值,故value恒为undefined。 -
修改属性初始值的方式:
属性装饰器可返回一个初始化函数,该函数会在属性实例化时自动执行,参数为属性的初始值,返回值即为属性的最终值,流程如下:- 步骤1:装饰器返回初始化函数(参数:初始值,返回值:最终值);
- 步骤2:类实例化时,将属性的初始值传入该函数;
- 步骤3:函数返回的结果作为属性的最终值。
示例(初始值翻倍):
// 装饰器:返回初始化函数,将初始值翻倍 function double() {return (initialValue: number) => initialValue * 2; } class C {@double field = 5; // 初始值5 → 初始化函数处理后→最终值10 } new C().field; // 10
问题3:当一个类的方法同时有多个装饰器(如@bound @log greet()),执行顺序如何?请结合“评估阶段”和“应用阶段”解释,并说明为何要遵循这一顺序?
答案:
-
执行顺序:
以@bound @log greet()为例,顺序分为两阶段:- 评估阶段:按装饰器从左到右的出现顺序评估,即先评估
@bound(计算bound表达式得装饰器函数),再评估@log(计算log表达式得装饰器函数); - 应用阶段:按装饰器从右到左的顺序应用,即先执行
@log(改写greet方法,添加日志逻辑),再执行@bound(对@log改写后的方法进行this绑定)。
最终效果:
greet方法先被添加日志,再绑定this,确保两者功能都生效。 - 评估阶段:按装饰器从左到右的出现顺序评估,即先评估
-
遵循该顺序的原因:
这一顺序符合“函数组合”的逻辑(内层装饰器先处理目标,外层装饰器再处理内层的结果),确保多个装饰器的功能不冲突且可叠加:- 若先执行
@bound再执行@log,则@log会改写@bound绑定后的方法,可能导致this绑定失效; - 按“评估左→右,应用右→左”的顺序,可保证每个装饰器都作用于前一个装饰器的处理结果,实现功能叠加(如“日志+
this绑定”同时生效)。
- 若先执行
