元数据编程 -- Reflect 与 metadata

1,590 阅读3分钟

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

前一篇文章介绍了 Reflect 基础 API 的内容,使用 Reflect 可以为对象设置属性,这个属性是会被直接添加到对象上的,有时候我们需要添加一种元数据,这种数据是用来描述对象本身,不应该出现在对象上。基于这种需求,有一个关于 metadata 的提案,这个提案在 Reflect 对象上扩展了元数据的能力。元数据是对反射功能的一个补充,在一些框架开发场景下很有用,由于这个功能还没有stable,我们可以引入 reflect-metadata 库来使用元数据能力。

meta-data 提案中包含以下方法:

  • Reflect.defineMetadata
  • Reflect.hasMetadata
  • Reflect.hasOwnMetadata
  • Reflect.getMetadata
  • Reflect.getOwnMetadata
  • Reflect.getMetadataKeys
  • Reflect.deleteMetadata

从方法名可以看出他们的作用,和常规的数据定义方法名风格是一致的,只是这里定义的是元数据信息元数据不影响程序运行,只能使用元数据相关 API 可以操控元数据。

查看 meta-data 源码可以看到实际上内部是利用一个 WeakMap 来实现的,元数据被存储在一个独立的 WeakMap 中,这样不影响程序本身,只在想获取时去读取即可。这里利用了 map 的数据结构可以接收对象作为 key 的特点。

除了之前提到的方法,Reflect 中还提供了一个装饰器,使用 @Reflect.metadata(metadataKey, metadataValue) 可以直接添加元数据,结合装饰器可以更容易进行元数据编程。使用 @Reflect.metadata 装饰器,我们可以很方便的对一个类的成员方法进行标识:

// Design-time type annotations
function Type(type) { return Reflect.metadata("design:type", type); }
function ParamTypes(...types) { return Reflect.metadata("design:paramtypes", types); }
function ReturnType(type) { return Reflect.metadata("design:returntype", type); }

// Decorator application
@ParamTypes(String, Number)
class C {
  constructor(text, i) {
  }

  @Type(String)
  get name() { return "text"; }

  @Type(Function)
  @ParamTypes(Number, Number)
  @ReturnType(Number)
  add(x, y) {
    return x + y;
  }
} 

这样通过 Reflect.getMetadata 就可以获取到标识的字段类型、参数类型、返回值类型。

在 typescript 中,编译器可以为我们自动添加上面的三种元数据类型:只需要在 tsconfig 文件中设置 emitDecoratorMetadata 值为 true 即可:

// typescript
class Demo {
  @LogMethod
  public foo(bar: number) {
    // do nothing
  }
}

// without emitDecoratorMetadata
class Demo {
    foo(bar) {
        // do nothing
    }
}
__decorate([
    LogMethod
], Demo.prototype, "foo", null);

// with emitDecoratorMetadata
class Demo {
    foo(bar) {
        // do nothing
    }
}
__decorate([
    LogMethod,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Number]),
    __metadata("design:returntype", void 0)
], Demo.prototype, "foo", null);

可以看到当开启 emitDecoratorMetadata 时,在装饰器中会自动添加 design:type、design:paramtypes、design:returntype 三种类型的元数据信息,我们可以通过 Reflect.getMetadata 直接获取相关的类型信息。

元数据编程多用于框架开发,有时需要实现动态加载,或者需要添加一些标识信息,这些场景通常会使用元数据来实现,开源框架 InversifyJSangularnest 中都有使用。

最后还是要注意这个提案还没进入标准,目前使用的是 reflect-metadata 库的实现版本,可能以后相关的 API 会有变化。