es7之Reflect Metadata

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据

想要了解其内容,我们来讲解几个概念。

  1. MetaData:也称元数据,元数据是用来描述数据的数据。举个例子:元数据概念其实是跟数据库的字段名(field)一致 —— 在传统的数据库中就天然包含元数据的概念。比如name,phone,它们就是元数据。
  2. Reflect:es6规范中,Reflect已存在,简单来说,这个API的作用就是可以实现对变量操作的函数化,也就是反射,具体可看阮一峰es6关于reflect的教程
  3. Decorator:装饰器,主要用来扩展类和类的方法,使其功能更强大。具体可看阮一峰es6关于decorator的教程。

由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上… 这就限制了 JS 中元编程的能力。【元编程:Symbol、Reflect 和 Proxy 是属于 ES6 元编程范畴的,能“介入”的对象底层操作进行的过程中,并加以影响。元编程中的 元 的概念可以理解为 程序 本身。”元编程能让你拥有可以扩展程序自身能力“】


JS 中对 Reflect Metadata 的诉求:

  1. 其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)
  2. 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。
  3. 为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;
  4. 元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;
  5. 对开发人员来说,定义新的元数据生成装饰器应该简洁

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)

  1. 通过装饰器声明方式创建,推荐的方式,也是很主流的一种方式(例子在上面基础用法)
  2. “事后”(类创建完后)再给目标对象创建元数据,代码如下
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 方法

image.pngimage.png

二、判断是否存在元数据(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()));

输出:

image.png

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

image.pngimage.png

如果你用 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
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值