TypeScript 装饰器完全指南

0 阅读4分钟

深入理解 TypeScript 装饰器的原理与应用,掌握这一强大的元编程工具。

什么是装饰器

装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上,用于修改其行为。它是 TypeScript 中的元编程工具,让我们能够在编译时动态修改代码结构。

核心价值: 将横切关注点(如日志、验证、缓存)从业务逻辑中分离出来,实现代码的声明式复用。

// ✅ 装饰器方式:声明式、可复用
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] ${key} 被调用`);
    return original.apply(this, args);
  };
}

class UserService {
  @Log
  createUser() {
    // 业务逻辑...
  }
}

装饰器基础

启用装饰器

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

装饰器类型

装饰器类型应用目标常见用途
类装饰器class单例模式、日志、Mixin
方法装饰器方法日志、性能监控、防抖
属性装饰器属性验证、依赖注入
参数装饰器参数验证、参数映射
访问器装饰器getter/setter缓存、验证

执行顺序

装饰器的执行顺序分为两个阶段,理解这一点非常重要:

1️⃣ 工厂函数执行阶段(从上到下,从外到内)

function A() {
  console.log('1. 工厂 A 被调用');
  return function (target: any) {
    console.log('2. 装饰器 A 被执行');
  };
}

function B() {
  console.log('3. 工厂 B 被调用');
  return function (target: any) {
    console.log('4. 装饰器 B 被执行');
  };
}

function C() {
  console.log('5. 工厂 C 被调用');
  return function (target: any) {
    console.log('6. 装饰器 C 被执行');
  };
}

@A()
@B()
@C()
class Example {}

输出:

1. 工厂 A 被调用
3. 工厂 B 被调用
5. 工厂 C 被调用
6. 装饰器 C 被执行
4. 装饰器 B 被执行
2. 装饰器 A 被执行

规律:

  • 工厂函数:从上到下执行(1→3→5)
  • 装饰器函数:从下到上执行(6→4→2)

2️⃣ 类成员装饰器的完整执行顺序

function ClassDec() {
  console.log('① 类装饰器工厂');
  return function (target: any) {
    console.log('⑥ 类装饰器执行');
  };
}

function PropDec() {
  console.log('② 属性装饰器工厂');
  return function (target: any, key: string) {
    console.log('④ 属性装饰器执行');
  };
}

function MethodDec() {
  console.log('③ 方法装饰器工厂');
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    console.log('⑤ 方法装饰器执行');
  };
}

@ClassDec()
class Example {
  @PropDec()
  name: string;

  @MethodDec()
  method() {}
}

完整执行顺序:

① 类装饰器工厂(最外层)
② 属性装饰器工厂
③ 方法装饰器工厂
④ 属性装饰器执行
⑤ 方法装饰器执行
⑥ 类装饰器执行(最内层)

3️⃣ 同一声明上的多个装饰器

function First() {
  console.log('工厂 First');
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    console.log('装饰器 First 执行');
  };
}

function Second() {
  console.log('工厂 Second');
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    console.log('装饰器 Second 执行');
  };
}

function Third() {
  console.log('工厂 Third');
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    console.log('装饰器 Third 执行');
  };
}

class Example {
  @First()
  @Second()
  @Third()
  method() {}
}

输出:

工厂 First    (从上到下)
工厂 Second
工厂 Third
装饰器 Third 执行    (从下到上)
装饰器 Second 执行
装饰器 First 执行

实际效果:

// 相当于:
method = First(Second(Third(
  Object.getOwnPropertyDescriptor(Example.prototype, 'method')
)));

4️⃣ 类成员的声明顺序

function logOrder(name: string) {
  return function (target: any, key: string) {
    console.log(`${name}: ${key}`);
  };
}

class Example {
  @logOrder('属性1')
  a: string;

  @logOrder('属性2')
  b: number;

  @logOrder('方法1')
  method1() {}

  @logOrder('方法2')
  method2() {}
}

输出:

属性1: a
属性2: b
方法1: method1
方法2: method2

规律: 按照声明顺序从上到下执行

📊 执行顺序总结表

阶段顺序说明
工厂函数调用⬇️ 从上到下最先执行,返回真正的装饰器函数
成员声明顺序⬇️ 从上到下属性、方法按代码声明顺序
装饰器函数应用⬆️ 从下到上后声明的先应用(类似洋葱模型)
实际调用时⬆️ 从外到内运行时最外层装饰器先执行

🎯 实用记忆口诀

工厂从上走到下
装饰从下往上挂
方法调用先外层
层层包裹像穿褂

⚠️ 注意事项

  1. 静态成员的装饰器先于实例成员执行
  2. 参数装饰器方法装饰器之前执行
  3. 装饰器的执行顺序影响功能,特别是日志、验证等场景

属性装饰器

基本语法

function PropertyDecorator(target: any, key: string) {
  // target: 实例属性为原型对象,静态属性为构造函数
  // key: 属性名字符串
}

实战:依赖注入

import "reflect-metadata";

function Inject(target: any, key: string) {
  const type = Reflect.getMetadata("design:type", target, key);
  target[key] = new type();
}

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

class UserService {
  @Inject
  logger!: Logger; // ! 表示"我会确保这个属性被赋值"

  createUser() {
    this.logger.log("用户创建成功");
  }
}

new UserService().createUser();
// 输出: [LOG] 用户创建成功

工作原理

TypeScript 编译器会为装饰的属性自动存储类型元数据:

class UserService {
  @Inject
  logger!: Logger;
}

// 编译后等同于:
class UserService {
  logger!: Logger;
}
// 自动添加:Reflect.metadata("design:type", Logger)(UserService.prototype, "logger");
// 然后调用:Inject(UserService.prototype, "logger");

确定赋值断言

logger!: Logger; // 告诉编译器:"我保证在使用前会被赋值"

为什么需要? TypeScript 默认要求属性要么初始化,要么标记为可选。使用 ! 可以避免这两个限制。

实战:属性验证

function Validate(rule: { minLength?: number; pattern?: RegExp }) {
  return function (target: any, key: string) {
    let value: any;
    Object.defineProperty(target, key, {
      get: () => value,
      set: (newValue: any) => {
        if (rule.minLength && newValue?.length < rule.minLength) {
          throw new Error(`${key} 长度不能少于 ${rule.minLength}`);
        }
        if (rule.pattern && !rule.pattern.test(newValue)) {
          throw new Error(`${key} 格式不正确`);
        }
        value = newValue;
      },
    });
  };
}

class User {
  @Validate({ minLength: 3, pattern: /^[a-zA-Z]+$/ })
  name!: string;
}

const user = new User();
user.name = "Jo"; // ❌ 抛出错误: name 长度不能少于 3

方法装饰器

基本语法

function MethodDecorator(
  target: any, // 实例方法:原型对象;静态方法:构造函数
  key: string, // 方法名
  descriptor: PropertyDescriptor, // 方法描述符
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    // 修改方法行为
    return original.apply(this, args);
  };
}

常用案例

1. 日志装饰器

function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] ${key} 被调用,参数:`, args);
    const result = original.apply(this, args);
    console.log(`[LOG] ${key} 返回:`, result);
    return result;
  };
}

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

new Calculator().add(1, 2);
// [LOG] add 被调用,参数: [1, 2]
// [LOG] add 返回: 3

2. 防抖装饰器

function Debounce(delay: number) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    let timer: NodeJS.Timeout | null = null;

    descriptor.value = function (...args: any[]) {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => original.apply(this, args), delay);
    };
  };
}

class SearchBox {
  query = "";

  @Debounce(300)
  search() {
    console.log("搜索:", this.query);
  }
}

3. 缓存装饰器

function Cache(ttl?: number) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    const cache = new Map<string, { value: any; expiry: number }>();

    descriptor.value = function (...args: any[]) {
      const cacheKey = JSON.stringify(args);
      const cached = cache.get(cacheKey);

      if (cached && (!ttl || Date.now() < cached.expiry)) {
        console.log("从缓存返回");
        return cached.value;
      }

      const result = original.apply(this, args);
      cache.set(cacheKey, {
        value: result,
        expiry: ttl ? Date.now() + ttl : Number.MAX_VALUE,
      });
      return result;
    };
  };
}

class MathService {
  @Cache(5000) // 缓存5秒
  fibonacci(n: number): number {
    console.log("计算 fibonacci");
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

4. 重试装饰器

function Retry(maxAttempts: number = 3, delay: number = 1000) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      let lastError: any;

      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return await original.apply(this, args);
        } catch (error) {
          lastError = error;
          if (attempt < maxAttempts) {
            await new Promise((resolve) => setTimeout(resolve, delay));
          }
        }
      }

      throw new Error(`${key} 失败 ${maxAttempts} 次后放弃`);
    };
  };
}

class ApiService {
  @Retry(3, 1000)
  async fetchData(url: string) {
    const response = await fetch(url);
    if (!response.ok) throw new Error("请求失败");
    return response.json();
  }
}

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,用于传递自定义参数。

// 装饰器工厂
function Debounce(delay: number) {
  // 返回真正的装饰器
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    let timer: NodeJS.Timeout | null = null;

    descriptor.value = function (...args: any[]) {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => original.apply(this, args), delay);
    };
  };
}

// 使用
class SearchBox {
  @Debounce(300) // 传递参数
  search() {}
}

链式装饰器

多个装饰器可以组合使用,从下到上依次执行:

class UserService {
  @Log()
  @Validate({ name: "string" })
  @Catch((error) => console.error(error))
  createUser(name: string) {
    // 先执行 Catch,再 Validate,最后 Log
  }
}

实际应用场景

1. NestJS 风格的依赖注入

import "reflect-metadata";

const Injectable = () => (_target: any) => {};

const constructors = new Map<string, any>();

function Inject(token?: string) {
  return function (target: any, key: string) {
    const type = Reflect.getMetadata("design:type", target, key);
    const dependencyToken = token || type.name;

    Object.defineProperty(target, key, {
      get() {
        if (!constructors.has(dependencyToken)) {
          throw new Error(`依赖 ${dependencyToken} 未注册`);
        }
        return constructors.get(dependencyToken);
      },
      set(value) {
        constructors.set(dependencyToken, value);
      },
    });
  };
}

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

@Injectable()
class Database {
  connect() {
    console.log("[DB] 连接数据库");
  }
}

class UserService {
  @Inject()
  logger!: Logger;

  @Inject()
  db!: Database;

  createUser() {
    this.logger.log("创建用户");
    this.db.connect();
  }
}

// 注册依赖
constructors.set("Logger", new Logger());
constructors.set("Database", new Database());

new UserService().createUser();
// [LOG] 创建用户
// [DB] 连接数据库

2. API 路由装饰器

const ROUTES: Array<{
  path: string;
  method: string;
  handler: Function;
}> = [];

function Controller(prefix: string) {
  return function <T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      constructor(...args: any[]) {
        super(...args);
        console.log(`📦 控制器注册: ${prefix}`);
      }
    };
  };
}

function Get(path: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    ROUTES.push({
      path,
      method: "GET",
      handler: descriptor.value,
    });
  };
}

function Post(path: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    ROUTES.push({
      path,
      method: "POST",
      handler: descriptor.value,
    });
  };
}

@Controller("/api/users")
class UserController {
  @Get("/:id")
  getUser(id: string) {
    return { id, name: "用户详情" };
  }

  @Post("/")
  createUser(data: any) {
    return { success: true, data };
  }
}

console.log("📋 已注册的路由:", ROUTES);
// 📦 控制器注册: /api/users
// 📋 已注册的路由: [
//   { path: '/:id', method: 'GET', handler: [Function: getUser] },
//   { path: '/', method: 'POST', handler: [Function: createUser] }
// ]

3. ORM 风格的实体定义

interface ColumnMetadata {
  type: "string" | "number" | "boolean" | "date";
  primary?: boolean;
  nullable?: boolean;
  length?: number;
}

const entityMetadata = new Map<Function, Map<string, ColumnMetadata>>();

function Entity(tableName: string) {
  return function (constructor: Function) {
    console.log(`📊 实体表名: ${tableName}`);
  };
}

function Column(options: ColumnMetadata) {
  return function (target: any, key: string) {
    if (!entityMetadata.has(target.constructor)) {
      entityMetadata.set(target.constructor, new Map());
    }
    entityMetadata.get(target.constructor)!.set(key, options);
  };
}

@Entity("users")
class User {
  @Column({ type: "number", primary: true })
  id!: number;

  @Column({ type: "string", length: 100 })
  name!: string;

  @Column({ type: "string", length: 255, nullable: true })
  email!: string | null;

  @Column({ type: "boolean", nullable: false })
  isActive!: boolean;
}

// 查看元数据
const userMetadata = entityMetadata.get(User);
console.log("📋 User 实体元数据:", userMetadata);
// 📊 实体表名: users
// 📋 User 实体元数据: Map {
//   'id' => { type: 'number', primary: true },
//   'name' => { type: 'string', length: 100 },
//   'email' => { type: 'string', length: 255, nullable: true },
//   'isActive' => { type: 'boolean', nullable: false }
// }

4. 表单验证

interface ValidationRule {
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  custom?: (value: any) => boolean | string;
}

const validations = new Map<Object, Map<string, ValidationRule[]>>();

function Validate(rules: ValidationRule | ValidationRule[]) {
  return function (target: any, key: string) {
    const ruleArray = Array.isArray(rules) ? rules : [rules];

    if (!validations.has(target)) {
      validations.set(target, new Map());
    }
    validations.get(target)!.set(key, ruleArray);

    let value: any;

    return {
      get() {
        return value;
      },
      set(newValue: any) {
        const errors: string[] = [];

        for (const rule of ruleArray) {
          if (rule.required && !newValue) {
            errors.push(`${key} 是必填项`);
          }
          if (rule.minLength && newValue?.length < rule.minLength) {
            errors.push(`${key} 长度不能少于 ${rule.minLength}`);
          }
          if (rule.maxLength && newValue?.length > rule.maxLength) {
            errors.push(`${key} 长度不能超过 ${rule.maxLength}`);
          }
          if (rule.pattern && !rule.pattern.test(newValue)) {
            errors.push(`${key} 格式不正确`);
          }
          if (rule.custom) {
            const result = rule.custom(newValue);
            if (result !== true) {
              errors.push(result as string);
            }
          }
        }

        if (errors.length > 0) {
          console.error(`❌ 验证失败 (${key}):`, errors.join(", "));
          throw new Error(errors.join(", "));
        }

        value = newValue;
      },
    };
  };
}

class RegisterForm {
  @Validate({
    required: true,
    minLength: 3,
    maxLength: 20,
    pattern: /^[a-zA-Z0-9_]+$/,
  })
  username!: string;

  @Validate({
    required: true,
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  })
  email!: string;

  @Validate([
    { required: true, minLength: 8 },
    {
      custom: (value) => /[A-Z]/.test(value) || "密码必须包含大写字母",
    },
    {
      custom: (value) => /[a-z]/.test(value) || "密码必须包含小写字母",
    },
    {
      custom: (value) => /[0-9]/.test(value) || "密码必须包含数字",
    },
  ])
  password!: string;
}

const form = new RegisterForm();
form.username = "user_123"; // ✅
form.email = "user@test.com"; // ✅
form.password = "Password123"; // ✅

// form.username = 'ab'; // ❌ username 长度不能少于 3
// form.email = 'invalid'; // ❌ email 格式不正确
// form.password = 'weak'; // ❌ 密码必须包含大写字母、小写字母、数字

最佳实践

✅ 应该做的

  1. 使用装饰器工厂提供参数

    function Log(prefix: string) {
      return function (
        target: any,
        key: string,
        descriptor: PropertyDescriptor,
      ) {
        // ...
      };
    }
    
  2. 保持装饰器简单专注

    // ✅ 职责单一
    @Log
    @Validate
    method() {}
    
    // ❌ 职责混乱
    @LogAndValidateAndCache
    method() {}
    
  3. 支持装饰器组合

    function Compose(...decorators: PropertyDecorator[]) {
      return function (
        target: any,
        key: string,
        descriptor: PropertyDescriptor,
      ) {
        decorators.forEach((decorator) => decorator(target, key, descriptor));
      };
    }
    
  4. 提供类型安全

    function TypedDecorator<T>(config: T) {
      return function (target: any, key: string) {
        // 使用 config 的类型信息
      };
    }
    
  5. 文档化装饰器行为

    /**
     * 防抖装饰器
     * @param delay 延迟时间(毫秒)
     * @example
     * class Example {
     *   @Debounce(300)
     *   handleInput() {}
     * }
     */
    function Debounce(delay: number) {}
    

❌ 不应该做的

  1. 不要在装饰器中执行副作用

    // ❌ 不好:装饰器执行副作用
    function BadDecorator(target: any, key: string) {
      console.log("执行了副作用");
      fetch("/api/log").then(/* ... */);
    }
    
    // ✅ 好:延迟副作用到方法调用时
    function GoodDecorator(
      target: any,
      key: string,
      descriptor: PropertyDescriptor,
    ) {
      const original = descriptor.value;
      descriptor.value = function (...args: any[]) {
        console.log("方法调用时执行副作用");
        return original.apply(this, args);
      };
    }
    
  2. 不要修改装饰器目标的结构

    // ❌ 危险:修改原型链
    function BadDecorator(target: any) {
      target.prototype.newMethod = () => {};
    }
    
    // ✅ 安全:只修改描述符
    function GoodDecorator(
      target: any,
      key: string,
      descriptor: PropertyDescriptor,
    ) {
      descriptor.writable = false;
    }
    
  3. 不要忘记保持 this 绑定

    // ❌ 错误:丢失 this 绑定
    descriptor.value = function (...args: any[]) {
      return original(...args); // this 指向错误
    };
    
    // ✅ 正确:保持 this 绑定
    descriptor.value = function (...args: any[]) {
      return original.apply(this, args);
    };
    
  4. 不要忽略错误处理

    // ✅ 好的做法
    function SafeDecorator(
      target: any,
      key: string,
      descriptor: PropertyDescriptor,
    ) {
      const original = descriptor.value;
      descriptor.value = async function (...args: any[]) {
        try {
          return await original.apply(this, args);
        } catch (error) {
          console.error(`装饰器错误: ${key}`, error);
          throw error;
        }
      };
    }
    

总结

关键要点

  1. 装饰器是编译时的元编程工具,用于声明式地修改类、方法、属性的行为
  2. Reflect Metadata 是装饰器的核心依赖,提供了运行时类型反射能力
  3. 装饰器工厂是传递参数的标准方式,使装饰器更加灵活可配置
  4. 组合优于配置,多个简单的装饰器比一个复杂的装饰器更好

🎯 掌握装饰器,让你的代码更优雅、更可维护!