首页 JavaScript reflect-metadata 包以及 ECMAScript 提案

reflect-metadata 包以及 ECMAScript 提案

0 1.6K

本文将介绍 reflect-metadata 包。TypeScript 使用这个包设计装饰器 decorator。这个包原本是为了提供Reflect API 的“元数据扩展” ECMAScript 提案的兼容方案。

元数据 metadata,简而言之,就是实际数据的额外信息(译注:通常被称为数据的数据)。例如,如果一个变量表示一个数组,那么,数组的长度就是一个元数据。类似的,数组中的每个元素都是实际数据,而这些元素的数据类型则是它们各自的元数据。可以这样认为,元数据并不是程序真正关心的数据,但是却能帮助程序更快更好地实现自己的目标。

下面是一个例子。如果你需要设计一个函数,用来输出其它函数的信息,那么,你会怎么设计?

// return information of a function
function funcInfo( func ) {
    return `Function "${ func.name }" accepts "${ func.length }" arguments.`;
}

// define sample functions
var add = ( a, b ) => a + b;
var sayHello = () => 'Hello World!';

// print function information
console.log( 'add info ->', funcInfo( add ) ); // add info -> Function "add" accepts "2" arguments.
console.log( 'sayHello info ->', funcInfo( sayHello ) ); // sayHello info -> Function "sayHello" accepts "0" arguments.

上面的例子,funcInfo函数接受一个函数作为参数,返回包含了这个参数函数的函数名参数个数的字符串。这些信息原本就包含在函数当中,虽然我们在大部分时间都不会使用这些信息。因此,func.namefunc.length就可以看作是元数据。

属性描述符一章,我们学习了对象属性的属性描述符。属性描述符是一个对象,定义了一个对象的属性的行为。例如,我们可以设置对象的属性是只读的或者不可枚举的。

属性描述符就像是对象属性的元数据。除非你专门使用例如Object.getPropertyDescriptor()或者Reflect.getPropertyDescriptor()这样的接口去访问,否则都看不到属性描述符。你可以通过修改属性描述符改变对象以及其属性的行为。

元数据让元编程变得可行。元数据是反射的必要条件,特别是内省机制。例如,你可以通过函数接收到的参数个数来改变函数的行为。

现在你了解了元数据的威力所在。它打开了元编程的大门。但是,我们在元编程方面受到了 JavaScript 所提供的接口限制。这一点,我们在之前的文章中介绍过。除非有方法给对象添加自定义元数据,否则我们实在无能为力。

而接下来要介绍的Reflect的元数据扩展正是为了解决这一问题。现在有一个提案,通过扩展Reflect的功能,允许向对象和对象属性添加自定义元数据。

但是,现在也不用太激动,因为这终究是一个提案,甚至还没有提交到 ECMAScript。然而,你可以在这里找到提案的详细信息;如果你奇怪为什么它还没有提交到 TC39,看看这里

如果你是 TypeScript 开发者,并且也在使用修饰器,那你一定听说过 reflect-metadata 包。这个包允许你向类、类属性等添加自定义元数据。但本章我们不会涉及到 TypeScript 或者修饰器。现在,我们只专注于 reflect-metadata 包。

Reflect 一文中,我们知道Reflect API 可以检查对象、添加元数据以改变其行为。例如,Reflect.has函数就像in运算符,用于检查属性是否存在于对象或对象的原型链。Reflect.setPropertyOf函数则可以给对象增加自定义属性,从而改变其行为。Reflect提供了很多类似的方法,用于实现反射机制

metadata 提案目的是通过新的方法扩展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函数允许给target对象或target对象的属性propertyKey添加新的元数据值metadataValue。这个值可以是任意 JavaScript 值。target对象则需要继承Object。你可以添加任意多的元数据值,这些值使用metadataKey进行区分。

// 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函数可以获取target对象或其属性上面,名为metadataKey的元数据。

另外的章节,我们提到过内部槽和内部方法。这些就是对象的内部属性和内部方法,用于保存数据,以及针对数据的操作逻辑。

metadata 提案为所有普通对象暴露内部槽[[Metadata]]。这个槽可以是null,表示target对象不包含任何元数据;否则,它是一个Map对象,包含target或其属性的不同元数据。

Reflect.defineMetadata调用内部方法[[DefineMetadata]]Reflect.getMetadata则使用[[GetMetadata]]获取。

下面看一个实际的例子。不过在此之前,我们需要安装 reflect-metadata 包。我们可以从 GitHub 依照安装说明安装,也可以使用npm install reflect-metadata安装。

require( 'reflect-metadata' );

// check if `Reflect.defineMetadata` is a `function`
console.log( 'check ->', typeof Reflect.defineMetadata ); // check -> function

// define a sample target
var target = { name: 'Ross' };

// add metadata to `target` and `target.name`
Reflect.defineMetadata( 'version', 1, target );
Reflect.defineMetadata( 'info', { props: 1 }, target );
Reflect.defineMetadata( 'is', 'string', target, 'name' );

// see the target
console.log( 'target ->', target ); // target -> { name: 'Ross' }

// extract metadata
console.log( 'target(info) ->', Reflect.getMetadata( 'info', target ) ); // target(info) -> { props: 1 }
console.log( 'target.name(is) ->', Reflect.getMetadata( 'is', target, 'name' ) ); // target.name(is) -> string

// when metadata is missing
console.log( 'target(missing) ->', Reflect.getMetadata( 'missing', target ) ); // target(missing) -> undefined

require( 'reflect-metadata' );将导入 reflect-metadata 包导入,这给Reflect对象在运行时增加了诸如defineMetadata这样的函数,我们可以通过检查Reflect.defineMetadata的类型得知是否正常。因此,本质上说,这个包其实是一个兼容方案 polyfill

然后,我们定义了一个对象target,然后在这个对象上添加了versioninfois元数据。其中,versioninfo直接添加到target对象,is添加到name属性。元数据值可以是任意 JavaScript 值。

Reflect.getMetadata返回关联到target或其属性的元数据。如果没有找到,则返回undefined。利用这些方法,我们可以将任意元数据关联到任意对象或属性。

从日志可以看出来,向target或其属性注册元数据并不会改变这个对象。事实上,元数据应该被保存在[[Metadata]]内部槽,而兼容方案中是使用的是WeakMap来保存target的元数据。

使用hasMetadata检查元数据是否存在,使用getMetadataKeys获取注册到target或其属性上面的所有元数据的键的集合。如果要删除元数据,则可以使用deleteMetadata方法。

// 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);
require( 'reflect-metadata' );

// define a sample target
var target = { name: 'Ross', age: 21 };

// add metadata to `target` and `target.name`
Reflect.defineMetadata( 'version', 1, target );
Reflect.defineMetadata( 'is', 'string', target, 'name' );

// check if metadata exists
console.log( 'has: target(version) ->', Reflect.hasMetadata( 'version', target ) ); // has: target(version) -> true
console.log( 'has: target.name(is) ->', Reflect.hasMetadata( 'is', target, 'name' ) ); // has: target.name(is) -> true
console.log( 'has: target.age(is) ->', Reflect.hasMetadata( 'is', target, 'age' ) ); // has: target.age(is) -> false

// get metadata keys
console.log( 'keys: target ->', Reflect.getMetadataKeys( target ) ); // keys: target -> [ 'version' ]
console.log( 'keys: target.name ->', Reflect.getMetadataKeys( target, 'name' ) ); // keys: target.name -> [ 'is' ]
console.log( 'keys: target.age ->', Reflect.getMetadataKeys( target, 'age' ) ); // keys: target.age -> []

// delete metedata key
console.log( 'delete: target(version) ->', Reflect.deleteMetadata( 'version', target ) ); // delete: target(version) -> true
console.log( 'delete: target.name(is) ->', Reflect.deleteMetadata( 'is', target, 'name' ) ); // delete: target.name(is) -> true
console.log( 'delete: target.age(is) ->', Reflect.deleteMetadata( 'is', target, 'age' ) ); // delete: target.age(is) -> false

默认情况下,getMetadatahasMetadatagetMetadataKeys会检查target的原型链,以便查找指定元数据键关联的值。因此,我们还有getOwnMetadatahasOwnMetadatagetOwnMetadataKeys这样的函数,只查找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);
require( 'reflect-metadata' );

// define a sample target with a custom prototype
var target = { name: 'Ross' };
var proto = { age: 21 };
Reflect.setPrototypeOf( target, proto );

// add metadata to `proto`
Reflect.defineMetadata( 'version', 1, proto );
Reflect.defineMetadata( 'is', 'number', proto, 'age' );

// get metadata
console.log( 'getMetadata: target(version) ->', Reflect.getMetadata( 'version', target ) ); // getMetadata: target(version) -> 1
console.log( 'getMetadata: target.age(is) ->', Reflect.getMetadata( 'is', target, 'age' ) ); // getMetadata: target.age(is) -> number
console.log( 'getOwnMetadata: target(version) ->', Reflect.getOwnMetadata( 'version', target ) ); // getOwnMetadata: target(version) -> undefined
console.log( 'getOwnMetadata: target.age(is) ->', Reflect.getOwnMetadata( 'is', target, 'age' ) ); // getOwnMetadata: target.age(is) -> undefined

// check if metadata exists
console.log( 'hasMetadata: target(version) ->', Reflect.hasMetadata( 'version', target ) ); // hasMetadata: target(version) -> true
console.log( 'hasMetadata: target.age(is) ->', Reflect.hasMetadata( 'is', target, 'age' ) ); // hasMetadata: target.age(is) -> true
console.log( 'hasOwnMetadata: target(version) ->', Reflect.hasOwnMetadata( 'version', target ) ); // hasOwnMetadata: target(version) -> false
console.log( 'hasOwnMetadata: target.age(is) ->', Reflect.hasOwnMetadata( 'is', target, 'age' ) ); // hasOwnMetadata: target.age(is) -> false

// check for metadata keys
console.log( 'getMetadataKeys: target ->', Reflect.getMetadataKeys( target ) ); // getMetadataKeys: target -> [ 'version' ]
console.log( 'getMetadataKeys: target.age ->', Reflect.getMetadataKeys( target, 'age' ) ); // getMetadataKeys: target.age -> [ 'is' ]
console.log( 'getOwnMetadataKeys: target ->', Reflect.getOwnMetadataKeys(  target ) ); // getOwnMetadataKeys: target -> []
console.log( 'getOwnMetadataKeys: target.age ->', Reflect.getOwnMetadataKeys( target, 'age' ) ); // getOwnMetadataKeys: target.age -> []

上面的例子,prototarget的原型,因此,所有在proto定义的元数据值都可以在target访问。然而,带有Own的函数不会检查proto的元数据。

提案还定义了Reflect.metadata函数,但并不是Reflect对象的方法。这个函数式一个修饰器工厂 decorator factory,这意味着调用这个函数时,它返回一个修饰器函数,可以用来修饰类或类属性。

我们在另外的文章中介绍过 JavaScript 修饰器。现在,JavaScript 修饰器还不是 ECMAScript 标准,这个提案处在 stage 3。因此,修饰器提案仍处于开发中。

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

在上面的例子中,@Reflect.metadata方法调用修饰了类MyClass和属性methodName方法。简单而言,它给这些实体添加了键为metadataKey的元数据metadataValue

如果你想知道它是怎么做到的,其实很简单。Reflect.metadata返回一个修饰器函数。这个修饰器函数内部实现了Reflect.defineMetadata,从而将元数据添加到了它所修饰的实体上面。

问题在于,reflect-metadata实现了修饰器提案的原始版本 legacy version。TypeScript 同样实现了这个版本的修饰器实现。你可以阅读这篇文章了解如何使用原始版本的修饰器实现。

由于我们不能在原生 JavaScript 中使用这个包,因此下面的例子是 TypeScript 的。

当然,你也可以使用这个 babel 插件转译为原生 JavaScript。

import 'reflect-metadata';

// define a custom decorator
function myDecorator( metaValue ) {
    return Reflect.metadata( 'returns', metaValue );
}

@Reflect.metadata( 'version', 1 )
class Person {
    fname: string;
    lname: string;

    constructor( fname, lname ) {
        this.fname = fname;
        this.lname = lname;
    }

    @myDecorator( { returns: 'string' } )
    getFullName() {
        return this.fname + ' ' + this.lname;
    }
}

// create an instance of Person
const person = new Person( 'Ross', 'Geller' );

// get metadata
console.log( 'Person(version) ->', Reflect.getMetadata( 'version', Person ) ); // Person(version) -> 1
console.log( 'person.getFullName(returns) ->', Reflect.getMetadata( 'returns', person, 'getFullName' ) ); // person.getFullName(returns) -> { returns: 'string' }

在这个例子中,@Reflect.metadataPerson类增加了元数据,之后,我们可以通过Reflect.getMetadata(<key>, Person)读取这些元数据。你也可以基于Reflect.metadata实现自己的修饰器工厂,例如myDecorator

至此,我们介绍了 reflect-metadata 包以及元数据提案。这个提案的用例是无穷无尽的。有人说,这是 JavaScript 元编程的圣杯。不过,我们不得不等待它变成 ECMAScript 提案的那天。

Proxy 那里,我们介绍了每个代理的handler都等价于Reflect的静态函数。你或许希望 reflect-metadata 能够提供针对Proxy的支持,但现在并没有。你可以在这里了解到关于这一特性的更多信息。

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2