JavaScript 装饰器

深入解析 JavaScript 装饰器提案:类装饰器、方法装饰器、属性装饰器的定义,以及如何使用 reflect-metadata 存储元数据。

为什么装饰器是 JavaScript 的重要特性

装饰器(Decorators)是 TC39 正在推进的 ECMAScript 提案,提供了一种修改类、属性、方法行为的元编程方式。

TypeScript、Angular、Babel 等早已支持装饰器,但 JavaScript 原生支持还在 Stage 3。这篇文章解析装饰器的概念和用法。


一、装饰器提案状态

1.1 当前状态

装饰器提案目前处于 Stage 3(Candidate),是接近最终版本的阶段。使用时需要配置相应的打包工具或启用实验性特性。

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

1.2 Babel 配置

// babel.config.js
module.exports = {
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }]
  ]
};

二、类装饰器

2.1 基本类装饰器

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

@sealed
class Person {
  constructor(name) {
    this.name = name;
  }
}

// 等价于:
// class Person { ... }
// sealed(Person);

2.2 装饰器工厂

// 返回装饰器函数的函数
function version(version) {
  return function(constructor) {
    constructor.VERSION = version;
  };
}

@version('1.0.0')
class App {
  static VERSION; // 被装饰器设置
}

console.log(App.VERSION); // '1.0.0'

三、方法装饰器

3.1 方法装饰器签名

function readonly(target, propertyKey, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class Circle {
  @readonly
  pi() { return 3.14159; }
}

3.2 实用方法装饰器

// 自动绑定方法到实例
function autobind(_, __, descriptor) {
  const original = descriptor.value;
  return {
    configurable: true,
    enumerable: false,
    get() {
      return original.bind(this);
    }
  };
}

class Counter {
  count = 0;

  @autobind
  increment() {
    this.count++;
    return this.count;
  }
}

const counter = new Counter();
const increment = counter.increment;
console.log(increment()); // 1

四、属性装饰器

4.1 属性装饰器签名

function format(formatString) {
  return function(target, propertyKey) {
    // 存储元数据
    Reflect.defineMetadata('format', formatString, target, propertyKey);
  };
}

class User {
  @format('YYYY-MM-DD')
  birthday;
}

4.2 reflect-metadata

需要使用 reflect-metadata 库存储元数据:

import 'reflect-metadata';

const formatMetadataKey = Symbol('format');

function format(formatString) {
  return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target, propertyKey) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class User {
  @format('YYYY-MM-DD')
  birthday;
}

console.log(getFormat(User.prototype, 'birthday')); // 'YYYY-MM-DD'

五、访问器装饰器

5.1 getter/setter 装饰器

function logged(target, name, descriptor) {
  const originalGet = descriptor.get;
  const originalSet = descriptor.set;

  descriptor.get = function() {
    console.log(`Reading ${name}`);
    return originalGet.call(this);
  };

  descriptor.set = function(value) {
    console.log(`Setting ${name} to ${value}`);
    return originalSet.call(this, value);
  };

  return descriptor;
}

class Point {
  #x = 0;
  #y = 0;

  @logged
  get x() { return this.#x; }
  @logged
  set x(value) { this.#x = value; }
}

六、装饰器组合

6.1 多个装饰器

function first() {
  console.log('first(): evaluated');
  return function(target, propertyKey, descriptor) {
    console.log('first(): called');
  };
}

function second() {
  console.log('second(): evaluated');
  return function(target, propertyKey, descriptor) {
    console.log('second(): called');
  };
}

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

// 输出:
// first(): evaluated
// second(): evaluated
// second(): called
// first(): called

6.2 装饰器执行顺序

装饰器从下到上(从离方法最近到最远)求值,从上到下(从最远到最近)调用。


七、实际应用场景

7.1 依赖注入

constInject.define = function(service) {
  return function(target, propertyKey) {
    Object.defineProperty(target, propertyKey, {
      get: () => Inject.get(service)
    });
  };
};

class UserService {
  @Inject('Database')
  db;
}

7.2 验证装饰器

function validate(target, propertyKey, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    const rules = validationRules.get(propertyKey);
    rules.forEach(rule => rule(args));
    return original.apply(this, args);
  };
  return descriptor;
}

八、面试高频考点

考点 1:装饰器提案状态

装饰器目前处于 Stage 3,需要配置才能使用。

考点 2:装饰器类型

类装饰器、方法装饰器、属性装饰器、访问器装饰器各有不同的函数签名。

考点 3:reflect-metadata

用于在装饰器中存储和读取元数据。


延展阅读