1. 什么是元数据(MetaData)
元数据是用来描述数据的数据(Data that describes other data)
2. reflect-matadata
2.1 目的
- 很多设计模式 比如组合 依赖注入 运行时类型断言,反射/镜像 测试等希望可以在保持原有class的一致性的前提下为class添加元数据。
- 一致性是很多工具和库使用元数据的原因
- 元数据产生的装饰器可以通过改变装饰器来进行组合
- 元数据不仅仅只能在对象上使用 也应该被代理Proxy通过相应的 traps所使用
- 元数据应该保持与其他语言以及 ECMAScript运行特性的一致性
2.2 API解读
2.2.1 decorate
第一个方法就是decorate 有好几个重载,一个一个来看
2.2.1.1 对类的装饰
/**
* 应用于一个装饰器的集合给目标对象
* @param decorators 装饰器的数组
* @param target 目标对象
* @returns 返回应用提供的装饰器后的值
* 注意: 装饰器应用是与array的位置方向相反, 为从右往左
*/
function decorate(decorators: ClassDecorator[], target: Function): Function;
那么这个 ClassDecorator 究竟是个什么数组呢?
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
该定义为: 这是一个Function, 只有一个参数Target, 也就是类的构造函数constructor, 返回值的类型与target的函数类型一致或为空, 也就是说, 如果是一个类的话, 续需返回这个类或者空. 注意, 该类型的定义不在reflect-matadata中, 而是在lib.es5.d.ts中, 也就表明该为es5的原生实现
举个例子
const classDecorator: ClassDecorator = target => {
target.prototype.sayName = () => console.log('override');
// return target 这里可以return也可以不return, 因为target是一个对象引用
}
export class TestClassDecrator {
constructor(public name = '') {
}
sayName() {
console.log(this.name)
}
}
Reflect.decorate([classDecorator], TestClassDecrator) // 对其进行装饰
const t = new TestClassDecrator('nihao')
t.sayName() // 'override'
注意: 在classDecorator中传入的target, 只能修改其prototype的方法, 不能修改其属性, 因为其属性是 read-only
2.2.1.2 对属性或者方法的装饰
function decorate(
decorators: (PropertyDecorator | MethodDecorator)[],
target: Object,
propertyKey: string | symbol,
attributes?: PropertyDescriptor
): PropertyDescriptor
顺便提一嘴, descriptor 分为两种, 一种是数据描述符, 一种是存取描述符
// 数据描述符
{
value: 'aaa',
configurable: true,
writable: true,
enumerable: true
}
// 存取描述符
{
get(){return 1},
set() {console.log('set')},
configurable: true,
enumerable: true
}
回到方法和属性装饰器 举例子
属性装饰器
const propertyDecorator: PropertyDecorator = (target, propertyKey) => {
const origin = target[propertyKey]
target[propertyKey] = () => {
origin.call(target)
console.log('added override')
}
}
class PropertyAndMethodExample {
static staticProperty() {
console.log('im static property')
}
method() {
console.log('im one instance method')
}
}
Reflect.decorate([propertyDecorator], PropertyAndMethodExample, 'staticProperty')
// test property decorator
PropertyAndMethodExample.staticProperty() // im static property \n added override
方法装饰器
const methodDecorator:MethodDecorator = (target, propertyKey, descriptor) => {
// 将其描述改为不可编辑
descriptor.configurable = false
descriptor.writable = false
return descriptor
}
// 获取原descriptor
let descriptor = Object.getOwnPropertyDescriptor(PropertyAndMethodExample.prototype, 'method')
// 获取修改后的descriptor
descriptor = Reflect.decorate([methodDecorator], PropertyAndMethodExample, 'method', descriptor)
// 将修改后的descriptor添加到对应的方法上
Object.defineProperty(PropertyAndMethodExample.prototype, 'method', descriptor)
// test method decorator
const example = new PropertyAndMethodExample
example.method = () => console.log('override') // 报错 Cannot assign to read only property 'method' of object '#<PropertyAndMethodExample>'
2.2.2 metadata
默认的元数据装饰器可以被用于类 类成员以及参数
注意: 如果 metadataKey 已经被定义在 target 或 target key 那么 metadataValue将会被覆盖
/**
* @param metadataKey 元数据入口的key
* @param metadataValue 元数据入口的value
* @returns: 装饰器函数
*/
function metadata(metadataKey: any, metadataValue: any): {
(target: Function): void;
(target: Object, properKey: string | Symbol): void
}
下面就使用一波
const nameSymbol = Symbol('lorry')
// 类元数据
@Reflect.metadata('class', 'class')
class MetaDataClass {
// 实例属性元数据
@Reflect.metadata(nameSymbol, 'nihao')
public name = 'origin'
// 实例方法元数据
@Reflect.metadata('getName', 'getName')
public getName() {
}
// 静态方法元数据
@Reflect.metadata('static', 'static')
static staticMethod () {
}
}
const value = Reflect.getMetadata('name', MetaDataClass)
const metadataInstance = new MetaDataClass
const name = Reflect.getMetadata(nameSymbol, metadataInstance, 'name')
const methodVal = Reflect.getMetadata('getName', metadataInstance, 'getName')
const staticVal = Reflect.getMetadata('static', MetaDataClass, 'staticMethod')
console.log(value,name,methodVal,staticVal)
// undefined nihao getName static
2.2.3 defineMetadata
该方法是 metadata 的定义版本 也就是非@版本 会多传一个参数 target 表示待装饰的对象
ts定义如下
/**
* @param metadataKey 设置或获取时的key
* @param metadataValue 元数据内容
* @param target 待装饰的target
* @param targetKey target的property
*/
function defineMetadata(metadataKey: any, metadataValue: any, target: Object, targetKey: string | symbol) :void
查看例子
class DefineMetadata {
static staticMethod () {}
static staticProperty = 'static'
getName() {}
}
const type = 'type'
Reflect.defineMetadata(type, 'class', DefineMetadata)
Reflect.defineMetadata(type, 'staticMethod', DefineMetadata.staticMethod)
Reflect.defineMetadata(type, 'method', DefineMetadata.prototype.getName)
Reflect.defineMetadata(type, 'staticProperty', DefineMetadata, 'staticProperty')
const t1 = Reflect.getMetadata(type, DefineMetadata)
const t2 = Reflect.getMetadata(type, DefineMetadata.staticMethod)
const t3 = Reflect.getMetadata(type, DefineMetadata.prototype.getName)
const t4 = Reflect.getMetadata(type, DefineMetadata,'staticProperty')
console.log(t1,t2,t3,t4) // class staticMethod method staticProperty
注意 t4定义和获取不一样的地方 比如 t2和t3都有两种写法, 一种是将target传为对应的对象且必须为对象 以t2为例 也可以写为
Reflect.defineMetadata(type, 'staticMethos', DefineMetadata, 'staticMethod')
const t2 = Reflect.getMetadata(type, DefineMetadata, 'staticMethod')
需要注意的是这两者并不能混合使用, 比如
Reflect.defineMetadata(type, 'staticMethos', DefineMetadata, 'staticMethod')
const t2 = Reflect.getMetadata(type, DefineMetadata.staticMethod)
是无法获取到对应的metadataValue的 原因是如果未传入property 会当 property当作undefined,此时该target的 metadata entries(本质是一个Map)中就有一个 {undefined: Map()}而传入了 property 就是在该 target下的propery属性下的 entries set一个 {propery: Map()} 具体拿t2来说 方法1
Reflect.defineMetadata(type, 'staticMethod', DefineMetadata.staticMethod)
这条语句是在DefineMetadata.staticMethod 下的元数据为
为啥呢????
方法2
Reflect.defineMetadata(type, 'staticMethos', DefineMetadata, 'staticMethod')
明显这两者不等价, 所以无法互换. 顺带一嘴, @Reflect.metadata的行为跟设置property一致, 也就是为方法2的实现, 使用方法1获取@Reflect.metadata的数据会为undefined
2.2.4 hasMetaData
该方法返回布尔值 表明该target或其原型链上有没有对应的元数据
/**
* @param metadataKey 元数据的key
* @param target 定义的对象
* @param targetKey 重载参数, 可选
* @returns 在target或其原型链上返回true.
*/
function hasMetadata(metadataKey: any, target: Object, targetKey?: symbol|string): boolean;
举个例子
const type = 'type'
class HasMetadataClass {
@Reflect.metadata(type, 'staticProperty')
static staticProperty = ''
}
Reflect.defineMetadata(type, 'class', HasMetadataClass)
const t1 = Reflect.hasMetadata(type, HasMetadataClass)
const t2 = Reflect.hasMetadata(type, HasMetadataClass, 'staticProperty')
console.log(t1, t2)
2.2.5 hasOwnMetadata
跟Object.prototype.hasOwnProperty类似, 是只查找对象上的元数据, 而不会继续向上查找原型链上的, 其余的跟hasMetadata一致
const type = 'type'
class Parent {
@Reflect.metadata(type, 'getName')
getName() {}
}
@Reflect.metadata(type, 'class')
class HasOwnMetadataClass extends Parent{
@Reflect.metadata(type, 'static')
static staticProperty() {}
@Reflect.metadata(type, 'method')
method() {}
}
const t1 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass)
const t2 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass, 'staticProperty')
const t3 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass.prototype, 'method')
const t4 = Reflect.hasOwnMetadata(type, HasOwnMetadataClass.prototype, 'getName')
const t5 = Reflect.hasMetadata(type, HasOwnMetadataClass.prototype, 'getName')
console.log(t1, t2, t3, t4, t5) // true true true false true
// 注意t4和t5的区别
2.2.6 getMetadata
这个属性在之前验证各个属性的时候就已经使用过了, 就是用于获取target的元数据值, 会往原型链上找
/**
* @param metadataKey 元数据key
* @param target 元数据定义的target
* @param targetKey 可选项, 是否选择target的某个key
* @returns 如果找到了元数据则返回元数据值, 否则返回undefined
*
*/
function getMetadata(metadataKey: any, target: Object, targetKey?: string | symbol ): any;
2.2.7 getOwnMetadata
与hasOwnMetadata和hasMetadata的区别一样, 是否往原型链上找
2.2.8 getMetadataKeys
类似Object.keys, 返回该target以及原型链上target的所有元数据的keys
const type = 'type'
@Reflect.metadata('parent', 'parent')
class Parent {
getName() {}
}
@Reflect.metadata(type, 'class')
class HasOwnMetadataClass extends Parent{
@Reflect.metadata(type, 'static')
static staticProperty() {}
@Reflect.metadata('bbb', 'method')
@Reflect.metadata('aaa', 'method')
method() {}
}
const t1 = Reflect.getMetadataKeys(HasOwnMetadataClass)
const t2 = Reflect.getMetadataKeys(HasOwnMetadataClass.prototype, 'method')
console.log(t1, t2) // ["type", "parent"] \n ["design:returntype", "design:paramtypes", "design:type", "aaa", "bbb"]
t1很好理解, 因为会向上找原型链的parent t2好像多了一些东西, design: 开头的, 先按下不表, 看看 ‘aaa’ 和 ‘bbb’ 的顺序是和我们添加的顺序是相反的, 还记得前面说过装饰器的顺序是从右到左的, 所以先应用的bbb, aaa再应用的design:xxx
2.2.9 getOwnMetadataKeys
跟getMetadataKeys 一样, 只是不向原型链中查找
2.2.10 deleteMetadata
用于删除元数据
/**
* @param metadataKey 元数据key
* @param target 元数据定义的对象
* @param targetKey 对象对应的key
* @returns 如果对象上有该元数据, 返回true, 否则返回false
*/
function deleteMetadata(metadataKey: any, target: Object, targetKey?:symbol|string): boolean;
const type = 'type'
@Reflect.metadata(type, 'class')
class DeleteMetadata {
@Reflect.metadata(type, 'static')
static staticMethod() {}
}
const res1 = Reflect.deleteMetadata(type, DeleteMetadata)
const res2 = Reflect.deleteMetadata(type, DeleteMetadata, 'staticMethod')
const res3 = Reflect.deleteMetadata(type, DeleteMetadata)
console.log(res1, res2, res3) // true true false
2.2.11 design:
还有一个问题 没有解决 就是之前说的在 getMetadataKey时出现的 design:xxx的内容时怎么来的 表示什么意思呢?
design:type 表示被装饰的对象是什么类型, 比如是字符串? 数字?还是函数等
design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该key
design:returntype 表示被装饰对象的返回值属性, 比如字符串,数字或函数等 来个例子