JavaScript 装饰器(Decorators)提案:基于元编程的函数式转换

JavaScript 装饰器(Decorators)提案:基于元编程的函数式转换

各位同仁,各位技术爱好者,大家好!

今天,我们将深入探讨 JavaScript 语言中一个极具变革性的提案:装饰器(Decorators)。这个提案,目前已达到 TC39 Stage 3 阶段,距离最终成为语言标准仅一步之遥。它为 JavaScript 带来了强大的元编程能力,并以一种函数式转换的优雅姿态,极大地提升了我们代码的表达力、可维护性和复用性。

1. 装饰器:何以为代码赋能?

想象一下,你正在构建一个复杂的系统,其中有许多类和方法。你可能会遇到一些横切关注点(Cross-Cutting Concerns),例如:

  • 日志记录: 记录方法的调用、参数和返回值。
  • 权限校验: 确保用户有权执行某个操作。
  • 性能监控: 测量方法的执行时间。
  • 数据验证: 在方法执行前验证输入参数。
  • 缓存: 缓存方法的计算结果以提高性能。
  • 依赖注入: 自动为类的属性提供所需的依赖服务。

在没有装饰器的情况下,我们通常会采用以下几种模式来处理这些问题:

  1. 手动包装: 在每个方法内部或外部手动添加逻辑。这会导致大量重复代码,难以维护。
  2. 继承: 创建基类来封装通用逻辑。但这会引入复杂的继承链,且JavaScript单继承的限制使得这种方式很快就会捉襟见肘。
  3. 高阶函数(Higher-Order Functions – HOFs): 将函数作为参数或返回值的函数。这对于纯函数非常有效,但对于类的方法,尤其是需要访问 this 上下文的情况,会变得相对繁琐。
  4. 高阶组件(Higher-Order Components – HOCs): 在 React 等框架中常见,用于包装组件以增强其功能。这是一种针对特定框架的模式。
  5. 代理(Proxies): 在运行时拦截对象操作。虽然强大,但通常用于更通用的运行时行为拦截,而不是声明式地修改特定成员。

这些方法各有优劣,但共同的痛点是:它们往往不够声明式,或者会侵入到业务逻辑中,使得代码难以阅读和理解。装饰器的出现,正是为了解决这些痛点,提供一种优雅、声明式、非侵入式的方式,在不修改原有代码结构的前提下,对类及其成员进行增强和转换。

那么,什么是 JavaScript 装饰器?
从最核心的层面来看,装饰器是一种特殊的声明,可以附加到类、方法、访问器(getter/setter)、属性(字段)以及自动访问器上。它本质上是一个函数,这个函数会在被装饰的目标定义时被调用,并接收关于目标的信息(元数据)。然后,它可以返回一个新的目标,或者修改原有的目标行为,从而实现对目标代码的“装饰”或“转换”。

2. 元编程与函数式转换:装饰器的核心思想

理解装饰器,必须抓住其背后的两大基石:元编程(Metaprogramming)函数式转换(Functional Transformation)

2.1 元编程:代码即数据

元编程是指编写能够操作其他代码的代码。它允许程序在运行时检查、修改甚至生成自身的结构和行为。在 JavaScript 中,我们已经通过 ProxyReflect API 接触到了运行时元编程。而装饰器则将元编程提升到了一个新的层次——定义时元编程

当你在一个类、方法或属性上应用装饰器时,JavaScript 引擎会在这些目标被定义(而不是运行时被调用)的阶段,将目标的信息传递给装饰器函数。装饰器函数接收这些信息,并可以决定如何修改目标。这种在代码定义时介入并改变其行为的能力,正是元编程的体现。

例如,一个装饰器可以:

  • 读取一个方法的名称。
  • 修改一个属性的默认值。
  • 替换一个类的构造函数。
  • 为一个方法添加额外的逻辑(例如在执行前和执行后)。

通过这种方式,我们得以在更高的抽象层面思考和操作代码,将重复的、通用的逻辑从业务代码中抽离出来,实现代码的“自我改造”。

2.2 函数式转换:纯净与组合

装饰器本质上是函数。它们接收一个输入(被装饰的目标及其上下文),并返回一个输出(新的目标或对原目标的修改)。这种“输入 -> 转换 -> 输出”的模式,完美契合了函数式编程的思想。

一个理想的装饰器,应当是一个纯函数:给定相同的输入,总是返回相同的输出,并且不产生任何副作用。然而,在实际应用中,装饰器的目的往往就是产生副作用(例如,修改类的行为)。因此,我们更强调其“转换”的特性:它将一个实体(类、方法等)转换成另一个具有增强功能的实体。

这种转换可以是:

  • 包装(Wrapping): 用一个新函数包装原有方法,添加前置/后置逻辑。
  • 替换(Replacing): 完全用一个新函数或新类替换原有目标。
  • 修改描述符(Modifying Descriptors): 调整属性的 writableenumerable 等特性。

装饰器可以像乐高积木一样进行组合。多个装饰器可以应用于同一个目标,形成一个转换管道。这种组合能力使得我们可以将复杂的行为分解为一系列简单的、可复用的装饰器,并通过声明式的方式将它们组装起来。

// 假设有日志和权限两个装饰器
@log('debug')
@authorize('admin')
class UserService {
  getUser(id: string) {
    // ... 获取用户逻辑
  }
}

这里,getUser 方法首先会被 @authorize('admin') 装饰器处理,然后其结果再被 @log('debug') 装饰器处理。这种从内到外(或从下到上)的函数式组合,使得代码的意图一目了然。

3. 装饰器语法与应用目标

装饰器的语法非常简洁:使用 @ 符号紧跟装饰器函数的名称,放置在被装饰目标之前。如果装饰器需要参数,它必须是一个返回装饰器函数的函数(即装饰器工厂)。

// 简单装饰器
@decoratorName
class MyClass {}

// 装饰器工厂
@decoratorFactory(arg1, arg2)
class AnotherClass {}

装饰器可以应用于以下几种目标:

  1. 类装饰器(Class Decorators): 装饰整个类。

    @sealed
    class Greeter {
      greeting: string;
      constructor(message: string) {
        this.greeting = message;
      }
      greet() {
        return "Hello, " + this.greeting;
      }
    }
  2. 方法装饰器(Method Decorators): 装饰类的方法。

    class MyService {
      @logMethod
      getData(id: string) {
        // ...
      }
    }
  3. 属性装饰器(Property Decorators,或称字段装饰器 Field Decorators): 装饰类的属性(字段)。

    class User {
      @maxLength(20)
      username: string;
    }
  4. 访问器装饰器(Accessor Decorators): 装饰类的 gettersetter

    class Point {
      private _x: number;
      private _y: number;
    
      constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
      }
    
      @immutable
      get x() { return this._x; }
      set x(value: number) { this._x = value; }
    
      @immutable
      get y() { return this._y; }
      set y(value: number) { this._y = value; }
    }
  5. 自动访问器装饰器(Auto-Accessor Decorators): 装饰使用 accessor 关键字声明的自动访问器字段(ES2022+ 提案)。

    class Counter {
      @observable
      accessor count = 0; // 自动生成 getter 和 setter
    }

装饰器的求值顺序:
当一个目标上应用了多个装饰器时,它们的求值顺序是从上到下,但应用顺序(执行顺序)是从下到上(或从内到外)。这意味着最靠近目标的装饰器会先被执行,其返回的结果会作为下一个装饰器的输入。

@D1
@D2
class MyClass {
  @M1
  @M2
  myMethod() {}
}

求值顺序:D1 -> D2 -> M1 -> M2
应用顺序:M2 作用于 myMethod,然后 M1 作用于 M2 的结果;D2 作用于 MyClass,然后 D1 作用于 D2 的结果。

4. 深入剖析装饰器种类与签名(TC39 Stage 3 提案)

这是理解现代 JavaScript 装饰器最关键的部分。TC39 提案经历了多次迭代,目前的 Stage 3 提案与早期版本(包括 TypeScript 早期实验性支持的版本)有显著差异。核心变化在于引入了 context 对象,并统一了不同类型装饰器的签名模式。

所有的装饰器函数都接收两个参数:valuecontext

  • value:被装饰目标的值。具体类型取决于被装饰目标的种类。
  • context:一个包含元数据的对象,描述了被装饰目标的类型、名称、是否静态、是否私有等信息,并提供了一些实用工具(如 addInitializer)。

4.1 类装饰器(Class Decorators)

签名:

(value: Function, context: {
  kind: 'class';
  name: string | symbol;
  addInitializer(initializer: (this: Function) => void): void;
}) => Function | void;
  • value:被装饰的类构造函数本身。
  • context.kind: 始终为 'class'
  • context.name: 类的名称。
  • context.addInitializer: 一个函数,允许你在类定义完成之后,但在类的实例首次创建之前,添加一个初始化函数。这个初始化函数会在 this 绑定到类本身的情况下执行。

返回:

  • 可以返回一个新的类构造函数,以替换原始类。
  • 可以返回 void(或 undefined),表示不替换原始类。

示例:为类添加静态属性和方法

function registerService(name: string) {
  return function <T extends { new(...args: any[]): {} }>(
    value: T,
    context: ClassDecoratorContext<T>
  ) {
    if (context.kind !== 'class') {
      throw new Error('registerService can only decorate classes.');
    }

    // 添加一个静态方法
    context.addInitializer(function(this: T) {
      console.log(`Service ${name} initialized for class ${context.name as string}.`);
    });

    return class extends value {
      static serviceName = name;
      static getInstance(...args: any[]): InstanceType<T> {
        console.log(`Getting instance of ${name}`);
        return new this(...args) as InstanceType<T>;
      }
    };
  };
}

@registerService('UserService')
class UserService {
  private users: { id: string; name: string }[] = [];

  constructor() {
    this.users = [{ id: '1', name: 'Alice' }];
    console.log(`UserService instance created.`);
  }

  getUser(id: string) {
    return this.users.find(u => u.id === id);
  }
}

// 通过装饰器添加的静态方法和属性
console.log(UserService.serviceName); // 输出: UserService
const service = UserService.getInstance(); // 输出: Getting instance of UserService, UserService instance created.
console.log(service.getUser('1')); // 输出: { id: '1', name: 'Alice' }

4.2 方法装饰器(Method Decorators)

签名:

(value: Function, context: {
  kind: 'method';
  name: string | symbol;
  static: boolean;
  private: boolean;
  addInitializer(initializer: (this: Class) => void): void;
}) => Function | void;
  • value:被装饰的方法函数本身。
  • context.kind: 始终为 'method'
  • context.name: 方法的名称。
  • context.static: true 如果是静态方法,false 如果是实例方法。
  • context.private: true 如果是私有方法,false 如果是公共方法。
  • context.addInitializer: 同类装饰器,用于在类定义时进行方法相关的初始化。

返回:

  • 可以返回一个新的函数,以替换原始方法。
  • 可以返回 void,表示不替换原始方法。

示例:记录方法调用日志

function logMethod(
  value: Function,
  context: ClassMethodDecoratorContext
) {
  if (context.kind !== 'method') {
    throw new Error('logMethod can only decorate methods.');
  }
  const methodName = String(context.name);

  function replacementMethod(this: any, ...args: any[]) {
    console.log(`[${methodName}] called with arguments: ${JSON.stringify(args)}`);
    const result = value.apply(this, args);
    console.log(`[${methodName}] returned: ${JSON.stringify(result)}`);
    return result;
  }

  return replacementMethod;
}

class Calculator {
  @logMethod
  add(a: number, b: number): number {
    return a + b;
  }

  @logMethod
  subtract(a: number, b: number): number {
    return a - b;
  }
}

const calc = new Calculator();
calc.add(5, 3);
// 输出:
// [add] called with arguments: [5,3]
// [add] returned: 8

calc.subtract(10, 4);
// 输出:
// [subtract] called with arguments: [10,4]
// [subtract] returned: 6

4.3 属性装饰器(Property Decorators / Field Decorators)

签名:

(value: undefined, context: {
  kind: 'field';
  name: string | symbol;
  static: boolean;
  private: boolean;
  addInitializer(initializer: (this: Class) => void): void;
}) => ((initialValue: unknown) => unknown) | void;
  • value:对于字段装饰器,value 始终是 undefined。这是因为字段的初始化发生在类构造函数的内部,在装饰器运行之后。
  • context.kind: 始终为 'field'
  • context.name: 字段的名称。
  • context.static: true 如果是静态字段,false 如果是实例字段。
  • context.private: true 如果是私有字段,false 如果是公共字段。
  • context.addInitializer: 同上。

返回:

  • 可以返回一个初始化函数 ((initialValue) => newValue)。这个函数在实例创建时,字段被赋值时调用,接收原始的初始值(如果有的话),并返回新的初始值。
  • 可以返回 void,表示不修改字段的初始化行为。

示例:为属性设置默认值或进行类型转换

function defaultIfUndefined<T>(defaultValue: T) {
  return function (
    value: undefined,
    context: ClassFieldDecoratorContext<any, T | undefined>
  ) {
    if (context.kind !== 'field') {
      throw new Error('defaultIfUndefined can only decorate fields.');
    }
    return function (initialValue: T | undefined) {
      return initialValue === undefined ? defaultValue : initialValue;
    };
  };
}

class UserProfile {
  @defaultIfUndefined('Guest')
  username: string;

  @defaultIfUndefined(0)
  age: number;

  email: string = '[email protected]'; // 显式初始化不会被覆盖

  constructor(username?: string, age?: number) {
    this.username = username!;
    this.age = age!;
  }
}

const user1 = new UserProfile();
console.log(user1.username); // 输出: Guest
console.log(user1.age);      // 输出: 0
console.log(user1.email);    // 输出: [email protected]

const user2 = new UserProfile('Alice', 30);
console.log(user2.username); // 输出: Alice
console.log(user2.age);      // 输出: 30

4.4 访问器装饰器(Accessor Decorators)

签名:

(value: { get: Function; set: Function; }, context: {
  kind: 'accessor';
  name: string | symbol;
  static: boolean;
  private: boolean;
  addInitializer(initializer: (this: Class) => void): void;
}) => { get?: Function; set?: Function; } | void;
  • value:一个对象,包含原始的 getset 函数。
  • context.kind: 始终为 'accessor'
  • context.name: 访问器的名称。
  • context.static: true 如果是静态访问器,false 如果是实例访问器。
  • context.private: true 如果是私有访问器,false 如果是公共访问器。
  • context.addInitializer: 同上。

返回:

  • 可以返回一个包含新 get 和/或 set 函数的对象,以替换原始的访问器。
  • 可以返回 void,表示不替换原始访问器。

示例:只读访问器

function readonlyAccessor(
  value: { get: Function; set: Function; },
  context: ClassAccessorDecoratorContext
) {
  if (context.kind !== 'accessor') {
    throw new Error('readonlyAccessor can only decorate accessors.');
  }
  const accessorName = String(context.name);
  console.log(`Making accessor ${accessorName} readonly.`);

  return {
    get: value.get,
    set: function (this: any, newValue: any) {
      console.warn(`Attempted to set readonly accessor '${accessorName}' to '${newValue}'. Operation ignored.`);
      // 不执行原始的 set 函数
    }
  };
}

class Product {
  private _price: number;
  private _name: string;

  constructor(name: string, price: number) {
    this._name = name;
    this._price = price;
  }

  @readonlyAccessor
  get price(): number {
    return this._price;
  }
  set price(value: number) {
    this._price = value;
  }

  get name(): string {
    return this._name;
  }
  set name(value: string) {
    this._name = value;
  }
}

const product = new Product('Laptop', 1200);
console.log(product.price); // 输出: 1200

product.price = 1500; // 尝试修改只读属性
// 输出: Attempted to set readonly accessor 'price' to '1500'. Operation ignored.
console.log(product.price); // 输出: 1200 (未改变)

product.name = 'Gaming Laptop'; // 普通属性可修改
console.log(product.name);    // 输出: Gaming Laptop

4.5 自动访问器装饰器(Auto-Accessor Decorators)

自动访问器是 ES2022+ 提案中的一个新特性,允许使用 accessor 关键字声明一个字段,它会自动生成对应的 gettersetter。装饰器可以作用于这个自动生成的访问器。

签名:

(value: { get: Function; set: Function; init: (instance: Class) => any; }, context: {
  kind: 'auto-accessor';
  name: string | symbol;
  static: boolean;
  private: boolean;
  addInitializer(initializer: (this: Class) => void): void;
}) => {
  get?: Function;
  set?: Function;
  init?: (instance: Class, initialValue: any) => any;
} | void;
  • value:一个对象,包含原始的 getset 函数和一个 init 函数。init 函数用于在实例创建时设置自动访问器的初始值。
  • context.kind: 始终为 'auto-accessor'
  • context.name: 自动访问器的名称。
  • context.static: true 如果是静态自动访问器,false 如果是实例自动访问器。
  • context.private: true 如果是私有自动访问器,false 如果是公共自动访问器。
  • context.addInitializer: 同上。

返回:

  • 可以返回一个包含新的 getset 和/或 init 函数的对象。
  • 可以返回 void,表示不替换原始自动访问器。

示例:在自动访问器上实现数据绑定/观察者模式

type Listener<T> = (newValue: T, oldValue: T) => void;

function observable<T>(
  value: { get: Function; set: Function; init: (instance: any) => T; },
  context: ClassAutoAccessorDecoratorContext<any, T>
) {
  if (context.kind !== 'auto-accessor') {
    throw new Error('observable can only decorate auto-accessors.');
  }
  const accessorName = String(context.name);

  // 为每个实例添加一个私有属性来存储监听器
  context.addInitializer(function(this: any) {
    if (!this.__listeners__) {
      this.__listeners__ = new Map<string | symbol, Listener<any>[]>();
    }
  });

  return {
    get: function(this: any) {
      return value.get.call(this);
    },
    set: function(this: any, newValue: T) {
      const oldValue = value.get.call(this);
      if (newValue !== oldValue) {
        value.set.call(this, newValue);
        const listeners = this.__listeners__.get(accessorName) || [];
        listeners.forEach((listener: Listener<T>) => listener(newValue, oldValue));
      }
    },
    init: function(this: any, initialValue: T) {
      // 可以在这里注册一个方法来添加监听器
      this[`on${String(accessorName).charAt(0).toUpperCase() + String(accessorName).slice(1)}Change`] = (
        listener: Listener<T>
      ) => {
        if (!this.__listeners__.has(accessorName)) {
          this.__listeners__.set(accessorName, []);
        }
        this.__listeners__.get(accessorName)!.push(listener);
      };
      return initialValue;
    }
  };
}

class Store {
  @observable
  accessor count = 0;

  @observable
  accessor message = "Hello";
}

const store = new Store();

// 注册监听器
store.onCountChange((newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`);
});
store.onMessageChange((newValue, oldValue) => {
  console.log(`Message changed from "${oldValue}" to "${newValue}"`);
});

store.count = 1; // 输出: Count changed from 0 to 1
store.count = 1; // 不会触发,因为值未改变
store.count = 5; // 输出: Count changed from 1 to 5

store.message = "World"; // 输出: Message changed from "Hello" to "World"

4.6 装饰器种类与签名总结

装饰器类型 context.kind value 类型 返回值类型 主要用途
类装饰器 'class' Function (类构造函数) Function | void (新类或不替换) 修改类定义、添加静态成员、注册类、工厂模式
方法装饰器 'method' Function (方法函数) Function | void (新方法或不替换) 增强方法行为、日志、缓存、权限校验、限流
属性装饰器 'field' undefined ((initialValue: unknown) => unknown) | void (初始化函数或不替换) 设置默认值、类型转换、数据绑定、ORM映射
访问器装饰器 'accessor' { get: Function; set: Function; } { get?: Function; set?: Function; } | void (新访问器或不替换) 缓存 getter、校验 setter、只读属性
自动访问器装饰器 'auto-accessor' { get: Function; set: Function; init: Function; } { get?: Function; set?: Function; init?: Function; } | void (新自动访问器或不替换) 数据绑定、响应式属性、属性初始化逻辑的增强

所有 context 对象都包含 name, static, private(对于类装饰器,staticprivate 不适用),以及 addInitializer 方法。

5. 装饰器工厂与组合

5.1 装饰器工厂(Decorator Factories)

当装饰器需要接收参数时,它必须是一个装饰器工厂。装饰器工厂是一个函数,它接收参数,然后返回一个真正的装饰器函数。

function log(level: 'info' | 'warn' | 'error') {
  return function (
    value: Function,
    context: ClassMethodDecoratorContext
  ) {
    if (context.kind !== 'method') {
      throw new Error('log can only decorate methods.');
    }
    const methodName = String(context.name);
    return function (this: any, ...args: any[]) {
      console[level](`[${methodName}] (${level.toUpperCase()}) called with: ${JSON.stringify(args)}`);
      const result = value.apply(this, args);
      console[level](`[${methodName}] (${level.toUpperCase()}) returned: ${JSON.stringify(result)}`);
      return result;
    };
  };
}

class MyService {
  @log('info')
  fetchData(id: string) {
    return { id, data: `Data for ${id}` };
  }

  @log('error')
  processError(err: Error) {
    throw err;
  }
}

const service = new MyService();
service.fetchData('123');
try {
  service.processError(new Error('Something went wrong'));
} catch (e) {
  // 错误被抛出,但日志已记录
}

5.2 装饰器组合(Decorator Composition)

多个装饰器可以应用于同一个目标,它们会像洋葱一样层层包裹。外层装饰器会接收内层装饰器处理后的结果。执行顺序是从下到上(或从内到外)。

// 简单模拟一个权限校验装饰器
function authorize(role: string) {
  return function (
    value: Function,
    context: ClassMethodDecoratorContext
  ) {
    if (context.kind !== 'method') throw new Error('authorize can only decorate methods.');
    const methodName = String(context.name);
    return function (this: any, ...args: any[]) {
      // 假设这里有获取当前用户角色的逻辑
      const currentUserRole = 'admin'; // 模拟当前用户角色
      if (currentUserRole !== role) {
        throw new Error(`Permission denied: User is not a ${role}`);
      }
      console.log(`[${methodName}] Authorization successful for role: ${role}`);
      return value.apply(this, args);
    };
  };
}

class AdminPanel {
  @log('info')
  @authorize('admin')
  deleteUser(userId: string) {
    console.log(`Deleting user: ${userId}`);
    return `User ${userId} deleted.`;
  }

  @log('warn')
  @authorize('moderator')
  banUser(userId: string) {
    console.log(`Banning user: ${userId}`);
    return `User ${userId} banned.`;
  }
}

const admin = new AdminPanel();

try {
  admin.deleteUser('user123');
  // 输出:
  // [deleteUser] Authorization successful for role: admin
  // [deleteUser] (INFO) called with: ["user123"]
  // Deleting user: user123
  // [deleteUser] (INFO) returned: "User user123 deleted."
} catch (e: any) {
  console.error(e.message);
}

try {
  admin.banUser('user456'); // 模拟当前用户是 admin,无法执行 moderator 权限的操作
} catch (e: any) {
  console.error(e.message); // 输出: Permission denied: User is not a moderator
}

在这个例子中,@authorize('admin') 先于 @log('info') 执行。如果授权失败,log 装饰器甚至不会有机会执行被装饰的方法。

6. 实用场景与高级模式

装饰器强大的声明式能力,使其在许多领域都有广泛应用。

6.1 日志与调试

除了上面演示的方法日志,我们还可以实现更复杂的日志策略,例如记录方法执行时间:

function measureExecutionTime(
  value: Function,
  context: ClassMethodDecoratorContext
) {
  if (context.kind !== 'method') throw new Error('measureExecutionTime can only decorate methods.');
  const methodName = String(context.name);
  return function (this: any, ...args: any[]) {
    const start = performance.now();
    const result = value.apply(this, args);
    const end = performance.now();
    console.log(`Method '${methodName}' executed in ${end - start}ms.`);
    return result;
  };
}

class ReportGenerator {
  @measureExecutionTime
  generateLargeReport(data: any[]): string {
    // 模拟耗时操作
    let result = '';
    for (let i = 0; i < 1000000; i++) {
      result += String(Math.random());
    }
    return `Report for ${data.length} items.`;
  }
}

const generator = new ReportGenerator();
generator.generateLargeReport([1, 2, 3]);
// 输出: Method 'generateLargeReport' executed in XXXms.

6.2 验证(Validation)

对方法参数或类属性进行验证是常见需求。

function validate(validatorFn: (...args: any[]) => boolean, errorMessage: string = 'Validation failed.') {
  return function (
    value: Function,
    context: ClassMethodDecoratorContext
  ) {
    if (context.kind !== 'method') throw new Error('validate can only decorate methods.');
    const methodName = String(context.name);
    return function (this: any, ...args: any[]) {
      if (!validatorFn(...args)) {
        throw new Error(`[${methodName}] Validation Error: ${errorMessage}`);
      }
      return value.apply(this, args);
    };
  };
}

function minLength(len: number) {
  return function (
    value: undefined,
    context: ClassFieldDecoratorContext<any, string>
  ) {
    if (context.kind !== 'field') throw new Error('minLength can only decorate fields.');
    const fieldName = String(context.name);
    context.addInitializer(function(this: any) {
        Object.defineProperty(this, fieldName, {
            get: () => this[`_decorated_${fieldName}`],
            set: (val: string) => {
                if (val.length < len) {
                    throw new Error(`Field '${fieldName}' must have at least ${len} characters.`);
                }
                this[`_decorated_${fieldName}`] = val;
            },
            enumerable: true,
            configurable: true
        });
    });
  };
}

class UserInput {
  @minLength(5)
  username: string = '';

  constructor(username: string) {
      this.username = username;
  }

  @validate((email: string) => /^[^s@]+@[^s@]+.[^s@]+$/.test(email), 'Invalid email format')
  sendEmail(email: string) {
    console.log(`Sending email to ${email}`);
  }
}

try {
  const user = new UserInput('JohnDoe');
  user.sendEmail('[email protected]'); // Works
  user.sendEmail('invalid-email'); // Throws error

} catch (e: any) {
  console.error(e.message); // [sendEmail] Validation Error: Invalid email format
}

try {
    const user2 = new UserInput('bob'); // Throws error
} catch (e: any) {
    console.error(e.message); // Field 'username' must have at least 5 characters.
}

注意:字段装饰器实现验证通常需要结合 addInitializerObject.defineProperty 来劫持 setter,因为 value 参数是 undefined。这比方法装饰器稍微复杂一些。

6.3 缓存(Memoization)

缓存方法的结果以避免重复计算是性能优化的常用手段。

function memoize(
  value: Function,
  context: ClassMethodDecoratorContext
) {
  if (context.kind !== 'method') throw new Error('memoize can only decorate methods.');
  const methodName = String(context.name);
  const cache = new Map<string, any>();

  return function (this: any, ...args: any[]) {
    const cacheKey = JSON.stringify(args); // 简单的缓存键生成
    if (cache.has(cacheKey)) {
      console.log(`[${methodName}] Cache hit for ${cacheKey}`);
      return cache.get(cacheKey);
    }

    console.log(`[${methodName}] Cache miss for ${cacheKey}, calculating...`);
    const result = value.apply(this, args);
    cache.set(cacheKey, result);
    return result;
  };
}

class Fibonacci {
  @memoize
  calculate(n: number): number {
    if (n <= 1) return n;
    return this.calculate(n - 1) + this.calculate(n - 2);
  }
}

const fib = new Fibonacci();
console.log(fib.calculate(10)); // 计算并缓存
console.log(fib.calculate(10)); // 从缓存获取
console.log(fib.calculate(5));  // 计算并缓存
console.log(fib.calculate(5));  // 从缓存获取

6.4 依赖注入(Dependency Injection – DI)

装饰器是实现轻量级 DI 的理想工具,尤其是在 TypeScript 环境中。

interface ILogger {
  log(message: string): void;
}

class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log(`[ConsoleLogger] ${message}`);
  }
}

// 模拟一个简单的DI容器
const container = new Map<string, any>();
container.set('ILogger', new ConsoleLogger());

function inject(token: string) {
  return function (
    value: undefined,
    context: ClassFieldDecoratorContext<any, any>
  ) {
    if (context.kind !== 'field') throw new Error('inject can only decorate fields.');
    const fieldName = String(context.name);
    // 在类初始化时从容器中获取依赖
    context.addInitializer(function(this: any) {
      if (!container.has(token)) {
        throw new Error(`Dependency for token '${token}' not found.`);
      }
      this[fieldName] = container.get(token);
    });
  };
}

class UserService {
  @inject('ILogger')
  private logger!: ILogger; // 使用 ! 告诉 TypeScript 编译器这个属性会在运行时被初始化

  getUser(id: string) {
    this.logger.log(`Fetching user with ID: ${id}`);
    return { id, name: 'Injected User' };
  }
}

const userService = new UserService();
console.log(userService.getUser('456'));
// 输出:
// [ConsoleLogger] Fetching user with ID: 456
// { id: '456', name: 'Injected User' }

6.5 框架集成(ORM, Routing)

许多框架(如 Angular、NestJS)已经广泛使用装饰器来定义组件、服务、路由、数据库模型等。虽然它们的实现可能基于旧版或自定义的装饰器语法,但核心思想是相通的。

// 模拟一个简化的ORM Column装饰器
function Column(options?: { name?: string; type?: string; primary?: boolean }) {
  return function (
    value: undefined,
    context: ClassFieldDecoratorContext<any, any>
  ) {
    if (context.kind !== 'field') throw new Error('Column can only decorate fields.');
    const fieldName = String(context.name);

    context.addInitializer(function(this: any) {
      // 在类的原型上存储元数据,供ORM框架读取
      const metadata = Reflect.getMetadata('columns', this.constructor) || [];
      metadata.push({
        propertyKey: fieldName,
        columnName: options?.name || fieldName,
        type: options?.type || 'string',
        primary: options?.primary || false,
      });
      Reflect.defineMetadata('columns', metadata, this.constructor);
    });
  };
}

// 模拟一个简化的路由装饰器
function Get(path: string) {
  return function (
    value: Function,
    context: ClassMethodDecoratorContext
  ) {
    if (context.kind !== 'method') throw new Error('Get can only decorate methods.');
    const methodName = String(context.name);

    context.addInitializer(function(this: any) {
      const routes = Reflect.getMetadata('routes', this.constructor) || [];
      routes.push({
        method: 'GET',
        path: path,
        handler: methodName,
      });
      Reflect.defineMetadata('routes', routes, this.constructor);
    });
  };
}

// 需要引入 'reflect-metadata' 库并在 tsconfig.json 中启用
// import 'reflect-metadata';

class UserEntity {
  @Column({ primary: true, type: 'uuid' })
  id!: string;

  @Column({ name: 'user_name' })
  name!: string;

  @Column()
  email!: string;
}

class UserController {
  @Get('/users')
  getAllUsers() {
    return 'List of all users';
  }

  @Get('/users/:id')
  getUserById(id: string) {
    return `User with ID: ${id}`;
  }
}

// 模拟框架启动时读取元数据
// console.log(Reflect.getMetadata('columns', UserEntity));
// console.log(Reflect.getMetadata('routes', UserController));

注意: 上述示例中的 Reflect.getMetadataReflect.defineMetadata 需要 reflect-metadata 库的支持,通常与 TypeScript 结合使用。这展示了装饰器与元数据反射机制的紧密结合。

7. 装饰器与现有模式的对比

特性/模式 描述 优点 缺点 装饰器对比
高阶函数 (HOF) 函数接收函数作为参数或返回函数。 灵活,可组合,函数式编程核心。 对于类方法,需要手动绑定 this,语法冗长。 装饰器是 HOF 的语法糖,将 HOF 模式应用于类和其成员,更加声明式和简洁。
高阶组件 (HOC) React 中常见,函数接收组件作为参数并返回新组件。 逻辑复用,关注点分离。 “HOC地狱”:多层嵌套导致组件树复杂,调试困难;命名冲突。 装饰器提供更简洁的 HOC 语法(如果用于类组件),可读性更高。对于函数式组件,HOC 仍是主流。
混入 (Mixins) 对象或类通过复制属性和方法来“混入”其他对象的行为。 简单实现代码复用。 来源不清晰,命名冲突,this 上下文问题,难以追踪行为。 装饰器提供更安全的组合方式,通过显式装饰而非隐式属性复制来增强行为。
代理 (Proxies) 在运行时拦截对象操作(如属性访问、方法调用)。 极其强大和灵活,可实现深度运行时行为控制。 作用于运行时,性能开销,调试困难,无法修改私有成员。 装饰器作用于定义时,更适合声明式地修改结构和行为。装饰器可以内部使用 Proxy 来实现运行时行为增强。
继承 通过 extends 关键字实现类之间的层次结构。 代码复用,多态性。 单继承限制,继承链过深导致代码僵硬,“脆弱的基类问题”。 装饰器提供横向扩展能力,避免深层继承,支持多重行为叠加。

8. 工具链与未来展望

8.1 Babel 支持

由于装饰器仍处于 Stage 3 阶段,浏览器原生支持尚需时日。目前,我们需要通过 Babel 进行转译。

  • 旧版/实验性装饰器: 如果你使用 TypeScript 早期版本或 Babel 的旧插件 @babel/plugin-proposal-decorators 且未配置 version,它默认会使用旧版 legacy 模式。

    // .babelrc
    {
      "plugins": [
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        "@babel/plugin-proposal-class-properties" // legacy decorators usually need this too
      ]
    }

    这种模式与当前 TC39 提案差异较大。

  • TC39 Stage 3 提案支持: 为了使用最新的提案语法和语义,需要指定 version: "2023-11"

    // .babelrc
    {
      "plugins": [
        ["@babel/plugin-proposal-decorators", { "version": "2023-11" }],
        ["@babel/plugin-transform-class-properties", { "loose": false }], // 确保 class fields 也符合标准
        ["@babel/plugin-transform-private-methods", { "loose": false }],
        ["@babel/plugin-transform-private-property-in-object", { "loose": false }]
      ]
    }

    请注意,为了正确转译私有字段和方法,可能需要额外的 Babel 插件。

8.2 TypeScript 支持

TypeScript 长期以来都有自己的实验性装饰器实现(通过 experimentalDecorators 编译器选项),这个实现是基于 TC39 提案的早期版本。这意味着在 TypeScript 中使用 @ 语法,其行为和签名与当前 Stage 3 提案是不同的。

TypeScript 实验性装饰器(旧版)的特点:

  • 类装饰器:function (target: Function)
  • 方法/访问器装饰器:function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor)
  • 属性装饰器:function (target: Object, propertyKey: string | symbol)

如何配置 TypeScript 以支持 TC39 Stage 3 装饰器:
TypeScript 5.2+ 版本开始支持 Stage 3 装饰器。你需要在 tsconfig.json 中配置:

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022", // 或更高版本
    "module": "esnext",
    "experimentalDecorators": false, // 禁用旧的实验性装饰器
    "useDefineForClassFields": true, // 确保类字段语义与标准一致
    "emitDecoratorMetadata": true, // 如果需要 reflect-metadata
    "lib": ["es2022", "dom"], // 确保包含所需的库
    // 启用新的装饰器语法
    "moduleResolution": "node"
  }
}

重要提示:

  • experimentalDecorators: false 是关键,它会禁用旧的实验性装饰器。
  • 你需要确保你的 TypeScript 版本至少是 5.2。
  • 即使 TypeScript 编译器支持,运行时仍需要 Babel 或其他转译器来处理,直到浏览器原生支持。

8.3 展望未来

一旦装饰器最终成为语言标准,它将对 JavaScript 生态系统产生深远影响:

  • 框架和库的演进: 现有框架(如 Angular、NestJS)将能够迁移到标准装饰器,提供更统一、更强大的 API。新的框架可能会围绕装饰器构建其核心功能。
  • 代码简洁性: 开发者能够以更少的代码实现更复杂的功能,提高代码可读性和可维护性。
  • 元编程的普及: 更多的开发者将接触并利用元编程的能力,编写更灵活、更可扩展的应用程序。
  • 跨领域应用: 从 Web 开发到 Node.js 后端,再到桌面应用(如 Electron),装饰器都将成为增强代码能力的利器。

9. 使用装饰器的考量与最佳实践

尽管装饰器功能强大,但也需审慎使用。

  • 适度使用: 装饰器能够提供抽象,但也可能隐藏复杂性。不要过度使用,以免代码变得难以理解和调试。
  • 清晰的职责: 每个装饰器都应该有清晰、单一的职责。这有助于提高可复用性和可测试性。
  • 避免副作用: 尽量使装饰器函数本身是纯净的。如果必须引入副作用(如修改类),请确保这些副作用是可控且文档化的。
  • 测试性: 被装饰的代码可能需要专门的测试策略。考虑如何测试装饰器本身,以及被装饰后的代码。
  • 性能影响: 装饰器主要在定义时执行,对运行时性能影响通常很小。但如果装饰器内部执行了复杂的计算或创建了昂贵的运行时代理,可能会有性能考量。
  • 文档和约定: 对于自定义的装饰器,务必提供清晰的文档说明其用途、参数和行为。在团队内部建立使用装饰器的约定。
  • 错误处理: 装饰器内部的错误处理同样重要。确保在发生错误时能够提供有意义的反馈。

装饰器,作为 JavaScript 元编程和函数式转换的融合,无疑是语言演进中的一个重要里程碑。它提供了一种强大、声明式的机制,使我们能够以更优雅的方式处理横切关注点,构建更可维护、可扩展的代码。随着其正式进入语言标准,它必将成为现代 JavaScript 开发中不可或缺的工具。理解其核心原理、不同类型装饰器的签名以及与现有工具链的集成,是充分发挥其潜力的关键。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注