装饰器:那个在代码里“贴标签”的黑魔法,到底有什么用?

1 阅读4分钟

你有没有在Angular或NestJS里见过@Component@Injectable这种稀奇古怪的“@符号”?它们就像给代码贴的“便利贴”,背后却能自动帮你做一堆事情。今天我们就来揭开TypeScript装饰器的神秘面纱,看看这个“贴标签”魔法到底怎么用,以及为什么它能让你少写几千行重复代码。

前言

想象你去餐厅吃饭,你在菜单上贴了个标签“@少油”,厨房看到后自动给你少放油。你又贴个“@加辣”,厨房又自动加辣。你只需要贴标签,厨房负责执行。

这就是装饰器。它是一种特殊的声明,可以附加在类、方法、属性、参数上,用来修改或增强它们的行为。你不用手动调用什么函数,只要贴上“标签”,背后的逻辑就会自动生效。

一、装饰器长啥样?先看个例子

在TypeScript里,装饰器以@expression的形式出现,expression是一个函数,会在运行时被调用。

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('调用了方法:', propertyKey);
}

class Person {
  @log
  sayHello() {
    console.log('Hello');
  }
}

const p = new Person();
p.sayHello();
// 输出:
// 调用了方法: sayHello
// Hello

你什么都没改,只是加了个@log,每次调用sayHello就会自动打印日志。这就是装饰器的魅力。

二、启用装饰器:别急,先开个开关

TypeScript的装饰器目前是实验性特性,需要在tsconfig.json里开启:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // 可选,用于反射元数据
  }
}

三、装饰器的四种类型

装饰器可以贴在四个地方:类、方法、访问器/属性、参数。每种都有不同的参数签名。

1. 类装饰器

作用在类上,通常用来修改或替换类的定义。

function addTimestamp<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    timestamp = new Date();
  };
}

@addTimestamp
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const user = new User('张三');
console.log(user); // User { name: '张三', timestamp: 2025-04-10... }

类装饰器接收一个参数:类的构造函数。你可以返回一个新类替换它,或者直接修改原型。

2. 方法装饰器

最常用,可以拦截、修改、替换方法。

function measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    const start = performance.now();
    const result = original.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} 耗时 ${end - start}ms`);
    return result;
  };
  return descriptor;
}

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

参数:

  • target:类的原型(静态方法则是构造函数)
  • propertyKey:方法名
  • descriptor:属性描述符(可以修改value、writable等)

3. 属性装饰器

作用在属性上,通常用于配合元数据做依赖注入或验证。

function format(formatStr: string) {
  return function(target: any, propertyKey: string) {
    let value: string;
    const getter = function() {
      return value;
    };
    const setter = function(newVal: string) {
      value = formatStr.replace('%s', newVal);
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Greeting {
  @format('Hello, %s')
  name: string;
}

属性装饰器只能拿到目标类和属性名,不能直接修改属性值,但可以通过Object.defineProperty替换getter/setter。

4. 参数装饰器

作用在函数参数上,常用于依赖注入框架(比如Angular)。

function paramLogger(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`参数位置 ${parameterIndex} 被装饰了`);
}

class UserService {
  getUser(@paramLogger id: number) {
    return { id };
  }
}

参数装饰器很少单独用,通常配合类装饰器或方法装饰器收集元数据。

四、装饰器工厂:给装饰器传参数

你看到@log@measure这些是不带参数的。如果想让装饰器接受配置,需要再包一层函数:

function log(prefix: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      console.log(`${prefix} 调用 ${propertyKey}`);
      return original.apply(this, args);
    };
  };
}

class Test {
  @log('DEBUG')
  doSomething() {
    console.log('执行');
  }
}

这就是装饰器工厂:外层函数接收参数,内层函数是真正的装饰器。

五、多个装饰器:从下往上,从右往左

当你在同一个目标上使用多个装饰器时,它们的执行顺序是:先执行靠近目标的(从下往上),再执行外层的

@classDecoratorA
@classDecoratorB
class MyClass {}

执行顺序:classDecoratorB 先执行,然后 classDecoratorA

方法上的装饰器类似:先执行参数装饰器,再执行方法装饰器,最后是类装饰器(但方法装饰器本身的调用顺序是从下往上)。

六、实战:用装饰器实现权限校验

假设你要写一个类,某些方法只有管理员能调用。你可以用装饰器优雅地实现:

function adminOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    if (!this.isAdmin) {
      throw new Error('无权限,需要管理员角色');
    }
    return original.apply(this, args);
  };
}

class UserController {
  isAdmin = false;

  @adminOnly
  deleteUser(id: number) {
    console.log(`删除用户 ${id}`);
  }
}

const ctrl = new UserController();
ctrl.deleteUser(1); // 报错:无权限
ctrl.isAdmin = true;
ctrl.deleteUser(1); // 成功

看,你只需要在需要权限的方法上贴个@adminOnly,逻辑自动注入。

七、装饰器的实际应用场景

  • 日志记录:自动打印方法入参、返回值、耗时。
  • 权限校验:检查当前用户角色。
  • 数据验证:验证方法参数格式。
  • 依赖注入:Angular、NestJS 里大量使用。
  • 性能监控:自动记录方法执行时间。
  • 重试机制:方法失败后自动重试。

八、注意事项与坑点

  1. 装饰器目前是实验特性,虽然Angular、NestJS等框架广泛使用,但未来ECMAScript标准可能会有所变化。
  2. 不能用在普通JS文件,必须在TS或Babel中启用。
  3. 属性装饰器不能直接修改属性值,需要通过Object.defineProperty替换getter/setter。
  4. 装饰器在类定义时执行,而不是实例化时。这意味着你不能依赖实例属性(比如this.isAdmin)来做静态分析,但可以在返回的函数中延迟读取。

九、总结:装饰器就像“代码贴纸”

  • 装饰器是给类、方法、属性、参数贴的“标签”。
  • 标签背后的函数会在运行时自动执行,修改目标的行为。
  • 装饰器工厂可以传参,实现定制化。
  • 多个装饰器从下往上执行。
  • 常见用途:日志、权限、验证、注入。

学会装饰器,你就能写出更声明式、更优雅的代码。很多框架的魔法背后,其实就是这些小小的“@”符号。

如果你觉得今天的“便利贴”魔法够神奇,点个赞让更多人看到。明天我们将开启浏览器渲染原理之旅,从输入URL到页面显示,中间到底发生了什么?我们明天见!