文章目录

TypeScript 装饰器:@decorator 语法与元数据

发布于 2026-04-05 19:24:52 · 浏览 14 次 · 评论 0 条

TypeScript 装饰器:@decorator 语法与元数据

装饰器是 TypeScript 提供的一种强大语法糖,它允许你在不修改原代码的情况下,为类、方法、属性或参数添加额外功能。想象一下给代码贴标签——你可以在代码定义的地方"挂载"一些额外行为,这些行为在程序运行时自动触发。


1. 装饰器的基本概念

装饰器的本质是一个函数,它接收目标对象作为参数,在函数体内可以执行任意逻辑。以 @ 符号开头,放在要装饰的目标之前。

@decorator
class MyClass {}

启用装饰器功能需要在 tsconfig.json 中配置:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

2. 类装饰器

类装饰器作用于类的构造函数,它的参数是被装饰类的构造函数本身。

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return "Hello, " + this.greeting;
  }
}

上述代码中,@sealed 装饰器锁定了类的构造函数和原型,防止后续添加或删除属性。定义装饰器函数后,直接在类名上方使用 @ 符号即可生效。


3. 装饰器工厂

如果你需要向装饰器传递参数,可以使用"装饰器工厂"——一个返回装饰器函数的函数。

function color(value: string) {
  return function(target: Function) {
    target.prototype.color = value;
  };
}

@color("blue")
class Car {
  brand: string;
}

工厂函数 color 接收参数 value,返回的匿名函数才是真正的装饰器。调用时,外层函数接收配置参数,内层函数接收被装饰目标。

这种模式让装饰器变得灵活:同一个装饰器可以根据不同参数产生不同效果。


4. 方法装饰器

方法装饰器应用于类的方法,它的参数包括:目标类、属性名、以及属性的描述符。

function enumerable(value: boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}

class Project {
  name: string;

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

  @enumerable(true)
  getName() {
    return this.name;
  }
}

@enumerable(true) 将方法设为可枚举。注意:属性描述符 PropertyDescriptor 包含 valuewritableenumerableconfigurable 等属性,你可以修改它们来改变方法的默认行为。


5. 属性装饰器

属性装饰器接收两个参数:目标类和属性名。与方法装饰器不同,它不接收属性描述符,因为属性还没有被初始化。

function format(formatStr: string) {
  return function(target: any, propertyKey: string) {
    let value: string;

    Object.defineProperty(target, propertyKey, {
      get: function() {
        return formatStr + value;
      },
      set: function(newValue: string) {
        value = newValue;
      },
      enumerable: true,
      configurable: true
    });
  };
}

class User {
  @format("User: ")
  name: string;
}

const user = new User();
user.name = "Alice";
console.log(user.name); // 输出: User: Alice

这个例子展示了属性装饰器的经典用法:拦截属性的 getter 和 setter,在访问或赋值时自动添加前缀。


6. 参数装饰器

参数装饰器用于装饰方法的参数,接收三个参数:目标类、方法名、以及参数在函数签名中的索引。

function required(target: any, propertyKey: string, parameterIndex: number) {
  const existingRequiredParameters: number[] = 
    Reflect.getMetadata("requiredParams", target, propertyKey) || [];

  existingRequiredParameters.push(parameterIndex);

  Reflect.defineMetadata(
    "requiredParams", 
    existingRequiredParameters, 
    target, 
    propertyKey
  );
}

class Person {
  greet(@required message: string) {
    console.log(message);
  }
}

参数装饰器通常与元数据配合使用,用于标记哪些参数是必需的。


7. 装饰器执行顺序

当多个装饰器同时作用于同一个目标时,执行顺序遵循以下规则:

  1. 从上到下求值装饰器工厂
  2. 从下到上执行装饰器函数
function first() {
  console.log("factory1");
  return function(target: any) {
    console.log("decorator1");
  };
}

function second() {
  console.log("factory2");
  return function(target: any) {
    console.log("decorator2");
  };
}

@first()
@second()
class Example {}

输出顺序是:factory1 -> factory2 -> decorator2 -> decorator1记住:求值阶段按书写顺序从上到下,执行阶段按相反顺序从下到上。


8. 反射元数据与 reflect-metadata

装饰器的一大用途是配合反射系统存储和读取元数据。首先需要安装 reflect-metadata

npm install reflect-metadata

在代码入口处引入:

import "reflect-metadata";

现在你可以使用 Reflect.defineMetadataReflect.getMetadata

import "reflect-metadata";

function Column(name: string) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata("columnName", name, target, propertyKey);
  };
}

class User {
  @Column("user_name")
  name: string;
}

const meta = Reflect.getMetadata("columnName", User.prototype, "name");
console.log(meta); // 输出: user_name

元数据机制让装饰器能够在运行时"记住"一些信息,这对于依赖注入、ORM 映射、参数验证等场景至关重要。


9. 实际应用场景

9.1 自动验证参数

结合参数装饰器和元数据,可以实现自动参数验证:

import "reflect-metadata";

function MinLength(min: number) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata("minLength", min, target, propertyKey);
  };
}

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    const minLength = Reflect.getMetadata("minLength", target, propertyKey);

    for (const arg of args) {
      if (typeof arg === "string" && arg.length < minLength) {
        throw new Error(`Argument too short. Minimum length: ${minLength}`);
      }
    }
    
    return originalMethod.apply(this, args);
  };
}

class Form {
  @MinLength(3)
  @validate
  setUsername(name: string) {
    console.log(`Username set to: ${name}`);
  }
}

const form = new Form();
form.setUsername("ab"); // 抛出错误:Argument too short. Minimum length: 3
form.setUsername("abc"); // 正常执行

9.2 依赖注入容器

装饰器还可以实现简易的依赖注入:

const Injectable = () => (target: any) => target;

const Container = {
  services: new Map<string, any>(),

  register(key: string, value: any) {
    this.services.set(key, value);
  },

  resolve(target: any): any {
    const token = Reflect.getMetadata("injectToken", target);
    return this.services.get(token);
  }
};

function Inject(token: string) {
  return function(target: any, propertyKey: string | symbol, index?: number) {
    Reflect.defineMetadata("injectToken", token, target);
  };
}

@Injectable()
class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

class UserService {
  @Inject("Logger")
  logger!: Logger;
  
  createUser(name: string) {
    this.logger.log(`Creating user: ${name}`);
  }
}

Container.register("Logger", new Logger());

const userService = new UserService();
userService.createUser("Bob");

10. 注意事项与最佳实践

装饰器目前处于 TC39 提案的 Stage 3 阶段,生产环境中使用需谨慎评估。如果你的项目依赖装饰器实现核心功能,建议锁定 TypeScript 版本以确保编译结果稳定。

装饰器不应用于业务逻辑的"副作用"——它们更适合基础设施层面的横切关注点,如日志、验证、缓存、权限控制等。将业务逻辑与装饰器逻辑分离,可以保持代码的可读性和可维护性。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文