属性和参数装饰器
欢迎来到本专栏的第二十八篇。在前面的篇章中,我们已经系统性地介绍了 TypeScript 装饰器的基础:如何启用实验性装饰器、类装饰器与方法装饰器的定义方式,以及它们在元编程场景中的典型用法。通过这些内容,我们逐步建立起一个认知框架:装饰器是一种声明式、非侵入式的能力扩展机制,可以在不污染业务逻辑的前提下,为代码注入额外行为。
在这一篇中,我们将把视角进一步下沉,聚焦到属性装饰器(Property Decorator)与参数装饰器(Parameter Decorator)。这两类装饰器作用于类成员的更细粒度层级,分别面向"属性本身"和"方法参数",在验证、日志、权限控制、缓存等横切逻辑(Cross‑Cutting Concerns)中具有不可替代的价值。
本文将遵循由浅入深的结构:
- 先明确属性装饰器与参数装饰器在 TypeScript 装饰器体系中的定位
- 再通过大量示例讲解它们的语法与运行机制
- 最后结合 AOP(面向切面编程)的思想,展示它们在真实项目中的实践方式
希望在读完本文后,你不仅能"会写"属性和参数装饰器,更能理解为什么要用、什么时候用,以及如何与其他装饰器协同使用。
一、属性装饰器与参数装饰器的定位
在 TypeScript 的装饰器模型中,不同装饰器承担着不同层级的职责:
- 类装饰器:关注整体结构与生命周期
- 方法装饰器:关注函数调用过程
- 属性装饰器:关注成员状态与读写行为
- 参数装饰器:关注函数输入
属性装饰器和参数装饰器都属于"微观层面"的装饰器,它们并不会直接改变类的外形,而是通过元编程的方式,为某个具体成员附加规则或元信息。
这正好与 AOP 的思想高度契合。AOP 的核心目标是:
将日志、验证、鉴权、缓存等横切逻辑,从核心业务中剥离出来。
属性和参数往往是这些横切逻辑的天然入口:
- 属性:是否可写?是否需要校验?是否需要监听变化?
- 参数:是否合法?是否具备权限?是否需要转换?
如果把这些逻辑直接写进 setter 或方法体中,代码会迅速膨胀。装饰器提供了一种声明式解决方案,让这些规则"贴"在成员定义上,而不是散落在实现细节中。
二、属性装饰器:控制属性的行为
2.1 基本语法与最小示例
属性装饰器会在类定义阶段执行,接收两个核心参数:
target:原型对象(实例属性)或构造函数(静态属性)propertyKey:属性名
一个最简单的例子是只读属性:
ts
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false,
});
}
class Example {
@readonly
name = 'TypeScript';
}
这种写法的本质,是在类定义阶段修改属性的描述符,从而限制后续实例的行为。
需要注意的是:实例字段并不会直接提供 PropertyDescriptor ,因此在属性装饰器中,往往需要手动调用 Object.defineProperty 来重写 getter / setter。
2.2 使用 getter / setter 注入行为
属性装饰器最常见的用途之一,是将普通字段"升级"为访问器属性。
ts
function logged(target: any, propertyKey: string) {
let value = target[propertyKey];
Object.defineProperty(target, propertyKey, {
get() {
console.log(`get ${propertyKey}:`, value);
return value;
},
set(newVal) {
console.log(`set ${propertyKey}:`, newVal);
value = newVal;
},
enumerable: true,
configurable: true,
});
}
class Demo {
@logged
title = 'hello';
}
此时,业务代码完全不需要感知日志逻辑,却自动获得了可观测能力。这也是 MobX、Vue 响应式系统常用的设计思路。
2.3 参数化属性装饰器
在真实项目中,装饰器往往需要配置能力,因此通常会写成装饰器工厂:
ts
function minLength(length: number) {
return function (target: any, propertyKey: string) {
let value: string;
Object.defineProperty(target, propertyKey, {
set(newVal: string) {
if (newVal.length < length) {
throw new Error(`min length is ${length}`);
}
value = newVal;
},
get() {
return value;
},
});
};
}
class User {
@minLength(5)
username = '';
}
这种模式在表单校验、配置约束等场景中极为常见。
三、参数装饰器:捕获方法输入
3.1 基本语法
参数装饰器作用于方法参数,接收三个参数:
targetpropertyKeyparameterIndex
ts
function logParam(target: any, propertyKey: string, index: number) {
console.log(`param ${index} of ${propertyKey}`);
}
class Service {
run(@logParam name: string, age: number) {}
}
需要特别注意的是:
参数装饰器不会在方法调用时执行,而是在类定义阶段执行。
因此,它的核心用途并不是直接改变参数值,而是收集元数据。
3.2 结合 Reflect Metadata
参数装饰器最常见的搭配,是 reflect-metadata:
ts
import 'reflect-metadata';
function minValue(min: number) {
return function (target: any, key: string, index: number) {
Reflect.defineMetadata('min', min, target, `${key}:${index}`);
};
}
随后由方法装饰器在运行时读取这些元数据并执行校验。
3.3 参数装饰器 + 方法装饰器 = AOP
ts
function validate(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
args.forEach((arg, index) => {
const min = Reflect.getMetadata('min', target, `${key}:${index}`);
if (min !== undefined && arg < min) {
throw new Error(`arg ${index} < ${min}`);
}
});
return original.apply(this, args);
};
}
class Processor {
@validate
execute(@minValue(10) count: number) {
return count * 2;
}
}
这正是 NestJS 中 @Body()、@Param()、@Query() 等参数装饰器的实现思想。
四、装饰器与 AOP 的结合实践
装饰器几乎是 TypeScript 世界中实现 AOP 的事实标准。
4.1 日志切面
ts
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log('call', key, args);
const res = original.apply(this, args);
console.log('result', res);
return res;
};
}
4.2 缓存切面
ts
function cache(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
const store = new Map();
descriptor.value = function (...args: any[]) {
const k = JSON.stringify(args);
if (store.has(k)) return store.get(k);
const res = original.apply(this, args);
store.set(k, res);
return res;
};
}
这些切面都可以通过装饰器自由组合,形成清晰、可维护的横切逻辑结构。
五、多装饰器的顺序与组合
- 同一位置的多个装饰器:自下而上应用
- 执行时:外层装饰器包裹内层逻辑
ts
class Example {
@log
@cache
calc(n: number) {
return n * n;
}
}
理解这一点,对于组合多个 AOP 切面至关重要。
六、风险与最佳实践
注意事项:
- 装饰器仍是实验性特性
- 过度使用会增加调试成本
- 元数据依赖运行时库
推荐实践:
- 用于横切逻辑,不要替代业务代码
- 明确装饰器职责,避免"万能装饰器"
- 在团队内形成统一规范
结语
属性装饰器与参数装饰器,是 TypeScript 装饰器体系中最精细、也最贴近业务的一环。它们让 AOP 从"方法级别"深入到"状态与输入级别",为验证、权限、日志等需求提供了优雅的实现方式。
在下一篇中,我们将进一步深入 Reflect Metadata 与运行时反射机制,敬请期待。