装饰器与元数据反射(4)元数据反射

本篇内容包括如下部分:

  1. 为什么JavaScript中需要反射
  2. 元数据反射API
  3. 基本类型序列化
  4. 复杂类型序列化

为什么JavaScript中需要反射?

关于反射的概念,摘自百度百科

在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

可见反射机制对于依赖注入、运行时类型断言、测试是非常有用的,同时随着基于JavaScript的应用做的越来越大,使得我们希望有一些工具和特性可以用来应对增长的复杂度,例如控制反转,运行时类型断言等。但由于JavaScript语言中没有反射机制,所以导致这些东西要么没法实现,要么实现的不如C#Java语言实现的强大。

强大的反射API允许我们可以在运行时测试一个未知的类,以及找到关于它的任何信息,包括:名称、类型、接口等。虽然可以使用诸如Object.getOwnPropertyDescriptor()Object.keys()查询到一些信息,但我们需要反射来实现更强大的开发工具。庆幸的是,TypeScript已经支持反射机制,来看看这个特性吧

元数据反射API

可以通过安装reflect-metadata包来使用元数据反射的API

npm install reflect-metadata;

若要使用它,我们需要在tsconfig.json中设置emitDecoratorMetadatatrue,同时添加reflect-metadata.d.ts的引用,同时加载Reflect.js文件。然后我们来实现装饰器并使用反射元数据设计的键值,目前可用的有:

  • 类型元数据:design:type
  • 参数类型元数据:design:paramtypes
  • 返回类型元数据:design:returntype

我们来通过一组例子来说明

1)获取类型元数据

首先声明如下的属性装饰器:

function logType(target : any, key : string) {
    var t = Reflect.getMetadata("design:type", target, key);
    console.log(`${key} type: ${t.name}`);
}

接下来将其应用到一个类的属性上,以获取其类型:

class Demo{ 
    @logType
    public attr1 : string;
}

这个例子将会在控制台中打印如下信息:

attr1 type: String

2) 获取参数类型元数据

声明参数装饰器如下:

function logParamTypes(target : any, key : string) {
    var types = Reflect.getMetadata("design:paramtypes", target, key);
    var s = types.map(a => a.name).join();
    console.log(`${key} param types: ${s}`);
}

然后将它应用在一个类方法的参数上,用以获取所装饰参数的类型:

class Foo {}
interface IFoo {}

class Demo{ 
    @logParameters
        param1 : string,
        param2 : number,
        param3 : Foo,
        param4 : { test : string },
        param5 : IFoo,
        param6 : Function,
        param7 : (a : number) => void,
    ) : number { 
        return 1
    }
}

这个例子的执行结果是:

doSomething param types: String, Number, Foo, Object, Object, Function, Function

3) 获取返回类型元数据

同样的我们可以使用"design:returntype"元数据键值,来获取一个方法的返回类型:

Reflect.getMetadata("design:returntype", target, key);

基本类型序列化

让我们回看上面关于"design:paramtypes"的例子,注意到接口IFoo和对象字面量{test: string}被序列化为Object,这是因为TypeScript仅支持基本类型的序列化,基本类型序列化规则如下:

  • number序列化为number
  • string序列化为string
  • boolean序列化为boolean
  • any序列化为Object
  • void序列化为undefined
  • Array序列化为Array
  • 元组Tuple序列化为Array
  • class序列化为类的构造函数
  • 枚举Enum序列化为number
  • 剩下的所有其他类型都被序列化为Object

接口和对象字面量可能在之后的复杂类型序列化中会被做具体的处理。

复杂类型序列化

TypeScript的团队为复杂类型的元数据序列化做出了努力。上面列出的序列化规则对基本类型依然适用,但对复杂类型提出了不同的序列化逻辑。如下是通过一个例子来描述所有可能的类型:

interface _Type {
  /** 
    * Describes the specific shape of the type.
    * @remarks 
    * One of: "typeparameter", "typereference", "interface", "tuple", "union" or "function".
    */
  kind: string; 
}

我们也可以找到用于描述每种可能类型的类,例如用于序列化通用接口interface foo<bar>

// 描述一个通用接口
interface InterfaceType extends _Type {
  kind: string; // "interface"

  // 通用类型参数. 可能为undefined.
  typeParameters?: TypeParameter[];

  // 实现的接口.
  implements?: Type[];

  // 类型的成员 可能为undefined.
  members?: { [key: string | symbol | number]: Type; };

  // 类型的调用标识. 可能为undefined.
  call?: Signature[];

  // 类型的构造标识. 可能为undefined.
  construct?: Signature[];

  // 类型的索引标识. 可能为undefined.
  index?: Signature[];
}

这里有一个属性指出实现了哪些接口

// 实现的接口
implements?: Type[];

这种信息可以用来在运行时验证一个实例是否实现了特定的接口,而这个功能对于一个依赖翻转容器特别的有用。

相关推荐