Skip to main content

装饰器

装饰器

装饰者模式:能够在不改变对象自身基础上,在程序运行期间给对象添加职责

装饰器只能针对类和类的属性/方法,不能直接作用于普通函数(由于存在函数声明提升,会导致不可预知的执行顺序)。

注意: 装饰器提案的历史较长,目前主要存在两个版本:

  1. 旧版(Stage 2 / Legacy):早期 TypeScript 和 Babel 实现的版本,广泛应用于 Angular、NestJS 和 MobX 6 之前的版本。其 API 签名通常为 (target, name, descriptor)
  2. 新版(Stage 3 / Standard):目前 ECMAScript 的正式提案版本(Stage 3,22年至今,钉子户实锤了),已在 TypeScript 5.0+ 中原生支持。新版 API 签名发生了变化(接收 valuecontext),且不再依赖 Object.defineProperty

下面的示例主要展示旧版(Legacy)装饰器的实现思想,因为目前大多数成熟的开源框架(如 NestJS)仍在使用此规范。如果你在现代项目中使用,请注意 TypeScript 的 experimentalDecorators 配置。

属性描述符(前置知识)

装饰器(Legacy 版本)本质是对 Object.defineProperty 的语法糖。要理解装饰器,必须先理解属性描述符——它是描述对象属性行为的元数据。

Object.getOwnPropertyDescriptor(obj, key) 可以拿到一个描述符对象,分两类,互斥

描述符类型必有字段选填字段备注
数据描述符valuewritableenumerableconfigurable描述一个具体的值
存取描述符getsetenumerableconfigurable描述 getter / setter

writablegetset 三者只能选一类——一个属性要么是数据描述符,要么是存取描述符,不能混。

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。

多场景同时出现的优先级:

  1. 类装饰器(@A class)最后执行
  2. 方法 / 属性装饰器先于类装饰器
  3. 参数装饰器最先执行(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。