Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。
想要了解其内容,我们来讲解几个概念。
- MetaData:也称元数据,元数据是用来描述数据的数据。举个例子:元数据概念其实是跟数据库的字段名(field)一致 —— 在传统的数据库中就天然包含元数据的概念。比如name,phone,它们就是元数据。
- Reflect:es6规范中,Reflect已存在,简单来说,这个API的作用就是可以实现对变量操作的函数化,也就是反射,具体可看阮一峰es6关于reflect的教程
- Decorator:装饰器,主要用来扩展类和类的方法,使其功能更强大。具体可看阮一峰es6关于decorator的教程。
由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上… 这就限制了 JS 中元编程的能力。【元编程:Symbol、Reflect 和 Proxy 是属于 ES6 元编程范畴的,能“介入”的对象底层操作进行的过程中,并加以影响。元编程中的 元 的概念可以理解为 程序 本身。”元编程能让你拥有可以扩展程序自身能力“】
JS 中对 Reflect Metadata 的诉求:
- 其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)
- 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。
- 为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;
- 元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;
- 对开发人员来说,定义新的元数据生成装饰器应该简洁
TypeScript 已经完整的实现了装饰器的声明生成元数据,后续的讲解默认都以 TS 环境。
安装
我们想要使用这个功能,可以借助仓库reflect-metadata,先 npm 安装这个库:
npm install reflect-metadata —save
TypeScript 支持为带有 装饰器 的声明 生成元数据。
在 tsconfig.json里启用emitDecoratorMetadata
基础用法
严格地说,元数据(metadata)和 装饰器(Decorator) 是 EcmaScript 中两个独立的部分。 然而,如果你想实现像是反射这样的能力,你总是同时需要它们。Reflect Metadata 的 API 可以用于类或者类的属性上。
import "reflect-metadata";
@Reflect.metadata('inclass', '1')
class Person {
@Reflect.metadata('inmethod', '2')
public speak(val: string): string {
return val;
}
}
console.log(Reflect.getMetadata('inclass', Person)) // '1'
console.log(Reflect.getMetadata('inmethod', new Person(), 'speak')); // '2'
对照这个例子,我们再引出 Metadata 的四个概念:
Metadata Key {Any}(简写 k) | 元数据的 Key,对于一个对象来说,它可以有很多元数据,每一个元数据都对应有一个 Key。一个很简单的例子就是说,你可以在一个对象上面设置一个叫做 ‘name’ 的 Key 用来设置他的名字,用一个 ‘created time’ 的 Key 来表示他创建的时间。这个 Key 可以是任意类型。在后面会讲到内部本质就是一个 Map 对象 |
---|---|
Metadata Value {Any} (简写 v) | 元数据的值,任意类型都行。 |
Target {Object} (简写 t) | 表示要在这个对象上面添加元数据 |
Property {String|Symbol} (简写 p) | 用于设置在哪个属性上添加元数据。大家可能会想,这个是干什么用的,不是可以在对象上面添加元数据了么?其实不仅仅可以在对象上面添加元数据,甚至还可以在对象的属性上面添加元数据。其实大家可以这样理,当你给一个对象定义元数据的时候,相当于你是默认指定了 undefined 作为 Property。 |
metadata 毕竟也属于 “数据”,那么对应的 API 就是跟数据库的 CURD 增删改查的操作相对应的。
对照上面的4个参数,我们来理解API会更容易:
namespace Reflect {
// 用于装饰器
metadata(k, v): (target, property?) => void
// 在对象上面定义元数据
defineMetadata(k, v, o, p?): void
// 是否存在元数据
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean
// 获取元数据
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any
// 获取所有元数据的 Key
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]
// 删除元数据
deleteMetadata(k, o, p?): boolean
}
一、创建元数据(Reflect.metadata/Reflect.defineMetadata)
- 通过装饰器声明方式创建,推荐的方式,也是很主流的一种方式(例子在上面基础用法)
- “事后”(类创建完后)再给目标对象创建元数据,代码如下
class Test {
public func(val: string): string {
return val;
}
}
Reflect.defineMetadata('a', '1111', Test); // 给类添加元数据
Reflect.defineMetadata('b', '22222', Test.prototype, 'func');// 给类的属性添加元数据
console.log(Reflect.getMetadata('a', Test)); // 1111
console.log(Reflect.getMetadata('b', Test.prototype, 'func')) // 22222
Reflect.metadata和Reflect.defineMetadata其最本质都是调用源码中 OrdinaryDefineOwnMetadata 方法
二、判断是否存在元数据(hasMetadata/hasOwnMetadata)
它们两者调用方式一样,唯一的区别是前者会包含原型链查找,后者不会查找原型链
console.log(Reflect.hasMetadata('a', Test)); //true
console.log(Reflect.hasOwnMetadata('a', Test));//true
console.log(Reflect.hasMetadata('b', Test, 'func'));//false
console.log(Reflect.hasOwnMetadata('b', Test.prototype, 'func'));//true
三、查询元数据(hasMetadata/hasOwnMetadata)
它们之间的区别前者会包含原型链查找,后者不会查找原型链
console.log(Reflect.getMetadata('a', Test)); //1111
console.log(Reflect.getOwnMetadata('a', Test)); //1111
console.log(Reflect.getMetadata('b', Test, 'func'));//undefined
console.log(Reflect.getOwnMetadata('b', Test.prototype, 'func'));//22222
四、删除元数据(deleteMetadata)
console.log(Reflect.deleteMetadata('a', Test)) //true
console.log(Reflect.deleteMetadata('b', Test.prototype, 'func'));//true
console.log(Reflect.deleteMetadata('a', Test));//false
具体应用
1、控制反转,依赖注入(对控制反转,依赖注入概念不清的可以看下这篇文章:点击链接
type Constructor<T = any> = new (...args: any[]) => T;
const Injectable = (): ClassDecorator => target => {};
class OtherService {
a = 1;
}
@Injectable()
class TestService {
constructor(public readonly otherService: OtherService) {}
testMethod() {
console.log(this.otherService.a);
}
}
const Factory = <T>(target: Constructor<T>): T => {
// 获取所有注入的服务
const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
const args = providers.map((provider: Constructor) => new provider());
return new target(...args);
};
Factory(TestService).testMethod(); // 1
2、controller和getter的实现
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target);
}
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
}
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');
@Controller('/test')
class SomeClass {
@Get('/a')
someGetMethod() {
return 'hello world';
}
@Post('/b')
somePostMethod() {
return 'zhangjing';
}
}
function isFunction(arg: any): boolean {
return typeof arg === 'function';
}
function isConstructor(arg: string) {
return arg === 'constructor';
}
function mapRoute(instance) {
const prototype = Object.getPrototypeOf(instance);
// 筛选出类的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype)
.filter(item => !isConstructor(item) && isFunction(prototype[item]))
return methodsNames.map(methodName => {
const fn = prototype[methodName];
// 取出定义的 metadata
const route = Reflect.getMetadata(PATH_METADATA, fn);
const method = Reflect.getMetadata(METHOD_METADATA, fn);
return {
route,
method,
fn,
methodName
}
})
}
console.log(Reflect.getMetadata(PATH_METADATA, SomeClass)); // '/test'
console.log(mapRoute(new SomeClass()));
输出:
3、获取类型
TS 中的 reflect-metadata 是经过扩展,额外给我们添加 3 个类型相关的元数据。之所以会有,这是因为我们在 TS 中开启了 emitDecoratorMetadata编译选项,这样 TS 在编译的时候会将类型元数据自动添加上去。这也是 TS 强类型编程带来的额外好处.
design:type | 被装饰的对象是什么类型, 比如是字符串? 数字? 还是函数 |
---|---|
design:paramtypes | 被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key |
design:returntype | 表示被装饰对象的返回值属性, 比如字符串,数字或函数等 |
在 vue-property-decorator,通过使用 Reflect.getMetadata API,Prop Decorator 能获取属性类型传至 Vue
如果你用 ES6 编程,需要自己加这 3 个元数据,代码如下:
// 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;
}
}
// Metadata introspection
let obj = new C("a", 1);
let type = Reflect.getMetadata("design:type", obj, "add"); //Function() {}
let paramTypes = Reflect.getMetadata("design:paramtypes", obj, "add"); // [Number, Number]
let returntype = Reflect.getMetadata("design:returntype", obj, "add"); // Number() {}
console.log(type, paramTypes, returntype);
添加元数据,让对象拥有一个新的 [[Metadata]] 内部属性,包含一个 Map,这个 Map 的 key 是属性的 key 或者 undefined,值是 源数据的 key 以及相应的 value 组成的 Maps。从数据结构上我们可以看出其设计理念也很清晰:给对象添加额外的信息,但是不影响对象的结构 —— 这一点很重要,当你给对象添加了一个原信息的时候,对象是不会有任何的变化的,不会多 property,也不会有的 property 被修改了。但却可以衍生出很多其他的用途(比如可以让装饰器拥有真正装饰对象而不改变对象的能力,让对象拥有更多语义上的功能)
具体存储的位置:
- 当在类 C 本身上使用 metadata 的时候,元数据会存储在 C.[[Metadata]] 属性中,其对应的 property 值是 undefined
- 通过类声明的静态成员(members)定义的源数据会存在 C.[[Metadata]], 以该属性(property)名作为 key。(上述例子我没用过静态成员去定义元数据,大家可以试试)
- 定义在类 C 实例成员上的元数据,那么元数据会存储在C.prototype.[[Metadata]] 属性中,以该属性(property)名作为 key