裝飾器與元數據反射(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[];

這種信息可以用來在運行時驗證一個實例是否實現了特定的接口,而這個功能對於一個依賴翻轉容器特別的有用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章