本文将介绍 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.name
和func.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
,然后在这个对象上添加了version
、info
和is
元数据。其中,version
和info
直接添加到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
默认情况下,getMetadata
、hasMetadata
和getMetadataKeys
会检查target
的原型链,以便查找指定元数据键关联的值。因此,我们还有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);
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 -> []
上面的例子,proto
是target
的原型,因此,所有在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.metadata
给Person
类增加了元数据,之后,我们可以通过Reflect.getMetadata(<key>, Person)
读取这些元数据。你也可以基于Reflect.metadata
实现自己的修饰器工厂,例如myDecorator
。
至此,我们介绍了 reflect-metadata 包以及元数据提案。这个提案的用例是无穷无尽的。有人说,这是 JavaScript 元编程的圣杯。不过,我们不得不等待它变成 ECMAScript 提案的那天。
在 Proxy 那里,我们介绍了每个代理的handler
都等价于Reflect
的静态函数。你或许希望 reflect-metadata 能够提供针对Proxy
的支持,但现在并没有。你可以在这里了解到关于这一特性的更多信息。