装饰器
装饰器
装饰者模式:能够在不改变对象自身基础上,在程序运行期间给对象添加职责
装饰器只能针对类和类的属性/方法,不能直接作用于普通函数(由于存在函数声明提升,会导致不可预知的执行顺序)。
注意: 装饰器提案的历史较长,目前主要存在两个版本:
- 旧版(Stage 2 / Legacy):早期 TypeScript 和 Babel 实现的版本,广泛应用于 Angular、NestJS 和 MobX 6 之前的版本。其 API 签名通常为
(target, name, descriptor)。- 新版(Stage 3 / Standard):目前 ECMAScript 的正式提案版本(Stage 3,22年至今,钉子户实锤了),已在 TypeScript 5.0+ 中原生支持。新版 API 签名发生了变化(接收
value和context),且不再依赖Object.defineProperty。下面的示例主要展示旧版(Legacy)装饰器的实现思想,因为目前大多数成熟的开源框架(如 NestJS)仍在使用此规范。如果你在现代项目中使用,请注意 TypeScript 的
experimentalDecorators配置。
属性描述符(前置知识)
装饰器(Legacy 版本)本质是对 Object.defineProperty 的语法糖。要理解装饰器,必须先理解属性描述符——它是描述对象属性行为的元数据。
Object.getOwnPropertyDescriptor(obj, key) 可以拿到一个描述符对象,分两类,互斥:
| 描述符类型 | 必有字段 | 选填字段 | 备注 |
|---|---|---|---|
| 数据描述符 | value、writable | enumerable、configurable | 描述一个具体的值 |
| 存取描述符 | get、set | enumerable、configurable | 描述 getter / setter |
writable、get、set 三者只能选一类——一个属性要么是数据描述符,要么是存取描述符,不能混。
const obj = { x: 1 };
// 1️⃣ 数据描述符
Object.getOwnPropertyDescriptor(obj, "x");
// { value: 1, writable: true, enumerable: true, configurable: true }
// 2️⃣ 用 defineProperty 创建存取描述符
const obj2 = {};
Object.defineProperty(obj2, "y", {
get() {
return this._y * 2;
},
set(v) {
this._y = v;
},
enumerable: true,
configurable: true,
});
obj2.y = 5;
console.log(obj2.y); // 10
旧版装饰器拿到
descriptor后,要么直接改它的value(替换方法),要么调用Object.defineProperty(target, name, descriptor)重新定义。新版(Stage 3)已经脱离这套底层机制,但理解它有助于看懂 NestJS、TypeORM 等老库源码。
类装饰器
API 签名(Legacy):function decorator(Target: Function): Function | void
- 接收类的构造函数作为唯一参数
- 可以返回一个新的构造函数(通常
extends Target),也可以直接修改原类
经典场景 1:给类挂元信息(NestJS @Controller 的思路)
function addTimestamp(Target) {
Target.prototype.createdAt = new Date();
// 不返回 = 直接修改原类
}
@addTimestamp
class Animal {}
const cat = new Animal();
console.log(cat.createdAt); // 2026-07-30T...
经典场景 2:包装构造过程(如日志、单例、依赖注入)
function log(Target) {
// 返回一个新类,继承原类,扩展行为
return class extends Target {
constructor(...args) {
console.log("实例化参数:", args);
super(...args);
}
};
}
@log
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const cat = new Animal("Hello kitty", 2);
// 实例化参数:["Hello kitty", 2]
console.log(cat.name); // Hello kitty
类装饰器只会在类定义时执行一次,且不能装饰普通函数——因为函数声明会提升,装饰器跑在它之前,行为不可预测。
属性/方法装饰器
API 签名(Legacy):function decorator(target, name, descriptor)
| 参数 | 含义 |
|---|---|
target | 类的原型(静态成员时是类本身) |
name | 被装饰的属性名(字符串) |
descriptor | 该属性的属性描述符(参见上一节) |
最常见的用法是替换 descriptor.value 来包一层行为(日志、防抖、缓存、权限校验等)。
// 防抖装饰器
function debounce(wait) {
return function (target, name, descriptor) {
const original = descriptor.value;
let timer = null;
descriptor.value = function (...args) {
clearTimeout(timer);
timer = setTimeout(() => original.apply(this, args), wait);
};
return descriptor;
};
}
class SearchInput {
@debounce(500)
handleInput(value) {
console.log(`发送网络请求搜索: ${value}`);
}
}
访问器装饰器(getter / setter):第三参数是存取描述符,没有 value 字段,操作的是 get / set:
function trace(target, name, descriptor) {
const originalGet = descriptor.get;
const originalSet = descriptor.set;
descriptor.get = function () {
console.log(`get ${name}`);
return originalGet?.call(this);
};
descriptor.set = function (v) {
console.log(`set ${name} =`, v);
originalSet?.call(this, v);
};
return descriptor;
}
class User {
constructor(age) {
this._age = age;
}
@trace
get age() {
return this._age;
}
set age(v) {
this._age = v;
}
}
const u = new User(18);
u.age; // get age
u.age = 20; // set age = 20
⚠️ 一个坑:TypeScript 中装饰器既可作用于数据属性也可作用于访问器,但同一属性上不能同时存在数据描述符和存取描述符。如果先
@trace装饰了一个get age(),再写u.age = 20触发的是set而非直接赋值。
装饰器工厂(Decorator Factory)
上面 @debounce(500) 这种"带参数"的写法,其实是用装饰器工厂实现的——外层函数接参数,返回一个真正的装饰器。这才是装饰器实战中的主流形态(毕竟大多数场景都要传参)。
// 通用形态
function decoratorFactory(...args) {
// 1️⃣ 参数预处理
return function (target, name, descriptor) {
// 2️⃣ 真正的装饰器逻辑
return descriptor;
};
}
实战中两个最常用的工厂模式:
// 模式 A:替换方法 + 保留原方法引用
function logger(prefix) {
return (target, name, descriptor) => {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log(`[${prefix}] ${name} called with`, args);
return original.apply(this, args);
};
return descriptor;
};
}
// 模式 B:只读包装
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
装饰器执行顺序
单属性上多个装饰器会按"由内向外"求值,按"由外向内"应用:
@A
@B
@C
class Foo {}
// 求值顺序:A → B → C
// 应用顺序:C → B → A(最靠近类的先执行)
形象理解:洋葱模型——最内层(C)先包裹原方法,然后 B 包 C,A 包 B。
多场景同时出现的优先级:
- 类装饰器(
@A class)最后执行 - 方法 / 属性装饰器先于类装饰器
- 参数装饰器最先执行(NestJS 中
@Body()拿参数就是这个)
使用场景(开源库实践)
- MobX (响应式状态管理):大量使用
@observable标记响应式状态,@action标记修改状态的动作,@computed标记派生属性。 - NestJS / Angular (依赖注入与路由):使用
@Controller('/users')、@Get()标记路由控制器,使用@Injectable()实现控制反转。 - Core-Decorators (工具库):提供了
@readonly、@debounce、@time(统计执行时间)等通用装饰器。
TypeScript 配置
要在 TS 中用装饰器,需要在 tsconfig.json 中开启:
{
"compilerOptions": {
// Legacy 装饰器(NestJS / MobX 5 等老库)
"experimentalDecorators": true,
// Standard(Stage 3)装饰器(TS 5.0+)
"target": "ES2022", // 或更高
},
}
- 只配
experimentalDecorators: true→ Legacy 模式 - 配
target: ES2022+且不设experimentalDecorators→ Standard 模式 - 两个不能同时开,会冲突
推荐策略:维护老项目(NestJS / TypeORM)配 Legacy;新项目直接用 Standard API。