返回首页

说说你对 TypeScript 中装饰器的理解?应用场景?

问题解析

装饰器是 TypeScript 的实验性特性,考察候选人对装饰器概念、分类、执行顺序以及实际应用(如 Angular、NestJS、类库开发)的理解。

核心概念

  • 装饰器(Decorator):一种特殊类型的声明,可以附加到类声明、方法、访问符、属性或参数上
  • 装饰器工厂:返回装饰器函数的函数,可接收参数
  • 装饰器组合:多个装饰器的执行顺序
  • 元数据反射reflect-metadata 库实现元数据存储

详细解答

1. 是什么

装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问符、属性或参数上。

是一种在不改变原类和使用继承的情况下,动态地扩展对象功能。

同样的,本质也不是什么高大上的结构,就是一个普通的函数,@expression 的形式其实是 Object.defineProperty 的语法糖。

expression 求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息作为参数传入。

2. 启用装饰器

由于装饰器是一个实验性特性,若要使用,需要在 tsconfig.json 文件中启用:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // 如果需要元数据支持
  }
}

3. 装饰器的分类

3.1 类装饰器

类装饰器应用于类构造函数,可以用来监视、修改或替换类定义。

function addAge(constructor: Function) {
  constructor.prototype.age = 18;
}

@addAge
class Person {
  name: string;
  age!: number;  // 非空断言

  constructor() {
    this.name = 'huihui';
  }
}

let person = new Person();
console.log(person.age); // 18

原理:上述代码实际等同于 Person = addAge(function Person() { ... })

3.2 方法/属性装饰器

方法装饰器接收 3 个参数:

  • target:对象的原型
  • propertyKey:方法的名称
  • descriptor:方法的属性描述符
function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(target);           // 原型对象
  console.log("prop " + propertyKey);  // 方法名
  console.log("desc " + JSON.stringify(descriptor));
  descriptor.writable = false;   // 设置为不可写
}

function property(target: any, propertyKey: string) {
  console.log("target", target);
  console.log("propertyKey", propertyKey);
}

class Person {
  @property
  name: string;

  constructor() {
    this.name = 'huihui';
  }

  @method
  say() {
    return 'instance method';
  }

  @method
  static run() {
    return 'static method';
  }
}

const xmz = new Person();
// xmz.say = function() { return 'edit' }; // Error: 只读属性

3.3 参数装饰器

参数装饰器接收 3 个参数:

  • target:当前对象的原型
  • propertyKey:参数的名称
  • index:参数数组中的位置
function logParameter(target: Object, propertyName: string, index: number) {
  console.log(target);        // 原型对象
  console.log(propertyName);  // 方法名
  console.log(index);         // 参数索引
}

class Employee {
  greet(@logParameter message: string): string {
    return `hello ${message}`;
  }
}

const emp = new Employee();
emp.greet('hello');

3.4 访问器装饰器

访问器装饰器使用起来方式与方法装饰一致:

function modification(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(target);
  console.log("prop " + propertyKey);
  console.log("desc " + JSON.stringify(descriptor));
}

class Person {
  private _name: string;

  constructor() {
    this._name = 'huihui';
  }

  @modification
  get name() {
    return this._name;
  }
}

4. 装饰器工厂

如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再返回一个函数:

function addAge(age: number) {
  return function(constructor: Function) {
    constructor.prototype.age = age;
  };
}

@addAge(10)
class Person {
  name: string;
  age!: number;

  constructor() {
    this.name = 'huihui';
  }
}

let person = new Person();
console.log(person.age); // 10

5. 装饰器执行顺序

当多个装饰器应用于一个声明上,由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用。

function f() {
  console.log("f(): evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("f(): called");
  };
}

function g() {
  console.log("g(): evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("g(): called");
  };
}

class C {
  @f()
  @g()
  method() {}
}

// 输出:
// f(): evaluated
// g(): evaluated
// g(): called
// f(): called

深入理解

元数据反射

使用 reflect-metadata 库可以实现元数据的存储和读取:

import 'reflect-metadata';

const requiredMetadataKey = Symbol('required');

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  const existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
  const method = descriptor.value!;
  descriptor.value = function (...args: any[]) {
    const requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
    if (requiredParameters) {
      for (const parameterIndex of requiredParameters) {
        if (parameterIndex >= args.length || args[parameterIndex] === undefined) {
          throw new Error('Missing required argument.');
        }
      }
    }
    return method.apply(this, args);
  };
}

class Greeter {
  greeting: string;

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

  @validate
  greet(@required name: string) {
    return `Hello ${name}, ${this.greeting}`;
  }
}

应用场景

1. 日志记录

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${key} with`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Result:`, result);
    return result;
  };
  return descriptor;
}

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

2. 权限控制

function requireAuth(roles: string[]) {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const currentUser = getCurrentUser();
      if (!currentUser || !roles.includes(currentUser.role)) {
        throw new Error('Unauthorized');
      }
      return originalMethod.apply(this, args);
    };
  };
}

class AdminController {
  @requireAuth(['admin', 'superadmin'])
  deleteUser(id: string) {
    // 删除用户逻辑
  }
}

3. 防抖/节流

function debounce(delay: number) {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    let timer: NodeJS.Timeout;
    descriptor.value = function (...args: any[]) {
      clearTimeout(timer);
      timer = setTimeout(() => {
        originalMethod.apply(this, args);
      }, delay);
    };
  };
}

class SearchComponent {
  @debounce(300)
  onSearch(keyword: string) {
    // 搜索逻辑
  }
}

4. Angular 框架

Angular 大量使用装饰器:

@Component({
  selector: 'app-hero',
  template: '<div>{{name}}</div>'
})
class HeroComponent {
  @Input() name: string;
  @Output() onSelect = new EventEmitter();

  constructor(private service: HeroService) {}

  @HostListener('click')
  handleClick() {
    this.onSelect.emit(this.name);
  }
}

最佳实践

  1. 谨慎使用:装饰器仍是实验性特性,API 可能变化
  2. 单一职责:每个装饰器只做一件事
  3. 类型安全:为装饰器参数和返回值添加类型
  4. 文档说明:装饰器会改变代码行为,需要清晰的文档
  5. 测试覆盖:装饰器逻辑需要充分的单元测试

面试要点

  1. 装饰器本质:普通函数,@expressionObject.defineProperty 的语法糖
  2. 启用方式tsconfig.json 中设置 experimentalDecorators: true
  3. 分类:类装饰器、方法/属性装饰器、参数装饰器、访问器装饰器
  4. 装饰器工厂:返回装饰器函数的函数,支持传参
  5. 执行顺序:由上至下求值,由下至上调用
  6. 应用场景
    • 日志记录
    • 权限控制
    • 防抖节流
    • 框架开发(Angular、NestJS)
  7. 元数据反射:使用 reflect-metadata 存储和读取元数据