【译】reflect-metadata及相关ECMAScript提案简介

1,222 阅读7分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。

前言

今天,我们会关注reflect-metadata这个由TypeScript使用并为装饰器设计的包。该包主要是为Reflect API的 metadata-extensionECMAScript提案提供polyfill。

元数据 (Metadata)

动机

Metadata(元数据),简单的说,就是为真正的数据提供额外的信息。例如,如果一个变量表示一个数组,那么数组的length就是其元数据。相似的,数组中的每个元素是数据的话,那么数据类型就是其元数据。更加宽泛的讲,元数据不仅仅是编程中的概念,它能够帮助我们更快的实现一些事情。

让我们以一个例子开始。如果你需要设计一个函数,该函数能够打印其他函数的相关信息,那么你会打印哪些信息呢?

image.png

在上面的例子中,funcInfo 接受一个函数作为参数并返回一个包含有该函数的函数名参数个数的字符串。该信息被包含在函数本身中,然而,我们几乎不会在实际生产中用都这些信息。因此,func.namefunc.length 可以被认为是元数据。

属性描述符的文章中,我们学习到一个对象属性的属性描述符是一个能够描述一个属性行为的对象,例如,你可以设置一个对象的属性为“只读”或“不可枚举”的。

属性描述符就相当于是对象属性的元数据。除非你利用 Object.getPropertyDescriptorReflect.getPropertyDescriptor主动查看,否则属性描述符不会显示出来。你可以在属性描述符中自定义元数据去改变对象的属性和行为。

元数据能够使元编程具有可能性。元数据对于反射来说是非常有必要的,尤其是对于内省来说。例如,你可以改变基于函数参数个数的程序行为。

你现在能够明白元数据所拥有的强大能力了吧?它为元编程打开了无限的可能性。然而,我们受限于JavaScript提供的不甚强大的元编程的能力。假设有一种特性可以告诉我们一个对象的自定义元数据,那么我们几乎可以解决所有的问题。

现在就来介绍一些Reflect的元数据扩展是干嘛的了。它是一项Reflect的提案,该提案允许为对象和对象的属性增加自定义的元数据。

等等!!!我们还不能够高兴地太早,因为它仍然是一项提案而还没有真正的被提交至ECMAScript标准中。然而,你能够在这里找到详细的提案标准Metadata Proposal - ECMAScript (rbuckton.github.io)如果你想知道为什么该提案还没有被提交至 ECMAScript标准中,你可以参考这个Github的issueMake it an official proposal for ES (?) · Issue #9 · rbuckton/reflect-metadata (github.com)

reflect-metadata

如果你是一名TypeScript的开发者的话,那么你应该已经使用过装饰器了,那么你大概率也听说过 reflect-metadata 这个包。这个包能够让你为类、类成员增加自定义的元数据,但是在本文中,我们不会专门讨论TypeScript的装饰器。本文中,我们将专注于reflect-metadata提供的能力和我们能够使用它做些什么。

Reflect 文章中,我们了解到 Reflect的API是进行数据查看和增加元数据以改变其行为的强大工具。例如: Reflect.has 方法与 in 操作符类似,他们的都能够检查一个属性是否存在于一个对象及其原型链上。Reflect.setPrototypeOf 方法可以为对象设置自定义的原型,因为可以改变其行为。Reflect 还为反射提供了一系列的方法。

元数据提案提出了一些新的方法来扩展Reflect的能力。这些增加的方法增强了JavaScript对于元编程的支持。

 // define metadata on a target object
Reflect.defineMetadata(
  metadataKey,
  metadataValue,
  target
); 

// define metadata on a target's property
Reflect.defineMetadata(
  metadataKey,
  metadataValue,
  target,
  propertyKey
);

Reflect.defineMetadata 该方法允许你可以为任意的JavaScript对象或者是JavaScript对象的某个属性增加自定义的元数据。

 // get metadata associated with target
let result = Reflect.getMetadata(
  metadataKey,
  target
); 

// get metadata associated with target's property
let result = Reflect.getMetadata(
  metadataKey,
  target,
  propertyKey
);

使用 Reflect.getMetadata方法你可以从关联了metadataKey的对象和属性中提取出相关的元数据。

在之前的文章中,我们学习到内插槽和内部方法。这些对象的内部方法和属性决定了其真实数据和实际的操作逻辑。

元数据提案中提议所有的对象都应该具有 [[Metadata]] 内部插槽。该内插槽的值可以是null,这也就是说 target 对象中不具有任何的元数据,或者内插槽的值是一个Map 对象,其中针对不同的key保存有不同的元数据。

Reflect.defineMetadata 调用 [[DefineMetadata]] 内部方法并且 Reflect.getMetadata 使用 [[GetMedata]] 内部方法来获取元数据。

让我们用一个例子来说明,在这之前,我们需要先安装 reflect-metadata 这个包。你可以通过npm install reflect-metadata 命令来安装它。

image.png

require('reflect-metadata')这个引入语句比较特别。它导入了reflect-metadata包中与新提案相关的方法,例如: defineMetadata等。因此,该包从技术上将算是一个polyfill

后续,我们使用version, info, is三个自定义的key自定义了一些元数据到 target对象上。versioninfo 直接定义到了对象本身上,而 is 则被添加到了 name 属性中。元数据的值可以是任意的JavaScript对象。

Reflect.getMetadata 返回一个与 target 或者是 target 属性相关联的元数据。如果没有元数据被找到,那么则直接返回undefined。使用该方法你可以找到与对象或者对象属性中相关联的元数据。

当你往对象或者其属性中注册元数据时,对象本身是不会发生任何改变的。实际上,metadata会被存储在[[Metadata]] 内部插槽中,但是reflect-metadata 作为一种polyfill的解决方案则是使用了WeakMap 来为 target 保存元数据。

你可以使用hasMetadata 方法来检查是否存在元数据并且使用getMetadataKeys 来获取所有注册在target 及其属性上的所有元数据的键值。如果你想要去掉一些元数据,那么你可以使用 deletaMedata 方法。

 // check for presence of a metadata key (returns a boolean )
let result = Reflect.hasMetadata(key, target);
let result = Reflect.hasMetadata(key, target, property); // get all metadata keys (returns an Array )
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, property); // delete metadata with a key (returns a boolean )
let result = Reflect.deleteMetadata(key, target);
let result = Reflect.deleteMetadata(key, target, property);

image.png

一般情况下,getMetadata, hasMetadata, getMetadataKeys 都会在对象的原型链上进行查找是否存在元数据。因此,我们同样有getOwnMetadata, hasOwnMetadata, getOwnMetadataKeys 这些方法来获取 target 对象本身的元数据。

 // get metadata value of an own metadata key
let result = Reflect.getOwnMetadata(key, target);
let result = Reflect.getOwnMetadata(key, target, property); 

// check for presence of an own metadata key
let result = Reflect.hasOwnMetadata(key, target);
let result = Reflect.hasOwnMetadata(key, target, property); 

// get all own metadata keys
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, property);

image.png

在上面的例子中,proto 对象是 target 对象的原型,因此proto对象上定义的所有元数据都可以通过target 对象获取到。然而*Own类似的方法却不能够通过target 对象找到 proto 对象的元数据。上面程序的运行结果如下:

image.png

提案也提出Reflect.metadata这个方法,但是它不是一个我们之前看到的Reflect方法。该方法时一个装饰器工厂,它意味着它被调用时需要一些参数并返回一个可装饰类或者类成员的装饰器

@Reflect.metadata(metadataKey, metadataValue)
class MyClass {
    @Reflect.metadata(metadataKey, metadataValue)
    methodName(){
        // ...
    }
}

在上述代码片段中,@Reflect.metadata方法称为MyClassmethodName 的装饰器。简单的说,它为这两个实体增加了元数据。

他们的工作原来十分的简单。Reflect.metadata 方法被调用并返回了一个装饰器函数。该装饰器函数内部使用了 Reflect.metadata来为其增加元数据。

image.png

如上图所示,@Reflect.metadata 成功的为 Person 类增加了元数据,并且我们后续成功的从其中获取了增加的元数据。你也可以创建你自己的装饰器工厂,例如myDecorator那样。

总结

以上几乎就是关于 reflect-metadata 包和元数据提案的内容了。该提案的应用几乎没有终点。有人会说这是JavaScript元编程中的圣杯。我们只需要等待并期待它称为ECMAScript标准中的一部分。