属性和参数装饰器

属性和参数装饰器

欢迎来到本专栏的第二十八篇。在前面的篇章中,我们已经系统性地介绍了 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 与运行时反射机制,敬请期待。

相关推荐
康王有点困2 小时前
Flink部署模式
java·大数据·flink
小二·2 小时前
Python Web 开发进阶实战:量子机器学习实验平台 —— 在 Flask + Vue 中集成 Qiskit 构建混合量子-经典 AI 应用
前端·人工智能·python
TTGGGFF2 小时前
控制系统建模仿真(十):实战篇——从工具掌握到工程化落地
前端·javascript·ajax
芒克芒克2 小时前
LeetCode 134. 加油站(O(n)时间+O(1)空间最优解)
java·算法·leetcode·职场和发展
馨谙2 小时前
shell编程三剑客------sed流编辑器基础应用大全以及运行示例
linux·运维·编辑器
huahailing10242 小时前
Spring 循环依赖终极解决方案:从原理到实战(附避坑指南)
java·后端·spring
驱动探索者2 小时前
Linux list 设计
linux·运维·list
郝学胜-神的一滴2 小时前
深入解析C/S架构与B/S架构:技术选型与应用实践
c语言·开发语言·前端·javascript·程序人生·架构
遇见火星2 小时前
在Linux中使用parted对大容量磁盘进行分区详细过程
linux·运维·网络·分区·parted