属性和参数装饰器

属性和参数装饰器

欢迎来到本专栏的第二十八篇。在前面的篇章中,我们已经系统性地介绍了 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 基本语法

参数装饰器作用于方法参数,接收三个参数:

  • target
  • propertyKey
  • parameterIndex
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 与运行时反射机制,敬请期待。

相关推荐
qq_124987075312 小时前
基于JavaWeb的大学生房屋租赁系统(源码+论文+部署+安装)
java·数据库·人工智能·spring boot·计算机视觉·毕业设计·计算机毕业设计
消失的旧时光-194312 小时前
Linux 入门核心命令清单(工程版)
linux·运维·服务器
我是伪码农12 小时前
Vue 2.3
前端·javascript·vue.js
短剑重铸之日12 小时前
《设计模式》第十一篇:总结
java·后端·设计模式·总结
艾莉丝努力练剑13 小时前
【Linux:文件】Ext系列文件系统(初阶)
大数据·linux·运维·服务器·c++·人工智能·算法
小天源13 小时前
Cacti在Debian/Ubuntu中安装及其使用
运维·ubuntu·debian·cacti
若鱼191913 小时前
SpringBoot4.0新特性-Observability让生产环境更易于观测
java·spring
夜郎king13 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
Trouvaille ~13 小时前
【Linux】TCP Socket编程实战(一):API详解与单连接Echo Server
linux·运维·服务器·网络·c++·tcp/ip·socket
觉醒大王13 小时前
强女思维:着急,是贪欲外显的相。
java·论文阅读·笔记·深度学习·学习·自然语言处理·学习方法