TypeScript 裝飾器

2022-04-21 09:27 更新

隨著TypeScript和ES6里引入了類,在一些場(chǎng)景下我們需要額外的特性來(lái)支持標(biāo)注或修改類及其成員。 裝飾器(Decorators)為我們?cè)陬惖穆暶骷俺蓡T上通過(guò)元編程語(yǔ)法添加標(biāo)注提供了一種方式。 Javascript里的裝飾器目前處在 建議征集的第一階段,但在TypeScript里已做為一項(xiàng)實(shí)驗(yàn)性特性予以支持。

注意? 裝飾器是一項(xiàng)實(shí)驗(yàn)性特性,在未來(lái)的版本中可能會(huì)發(fā)生改變。

若要啟用實(shí)驗(yàn)性的裝飾器特性,你必須在命令行或tsconfig.json里啟用experimentalDecorators編譯器選項(xiàng):

命令行:

tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

裝飾器

裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明,方法, 訪問(wèn)符屬性參數(shù)上。 裝飾器使用@expression這種形式,expression求值后必須為一個(gè)函數(shù),它會(huì)在運(yùn)行時(shí)被調(diào)用,被裝飾的聲明信息做為參數(shù)傳入。

例如,有一個(gè)@sealed裝飾器,我們會(huì)這樣定義sealed函數(shù):

function sealed(target) {
    // do something with "target" ...
}

注意? 后面類裝飾器小節(jié)里有一個(gè)更加詳細(xì)的例子。

裝飾器工廠

如果我們要定制一個(gè)修飾器如何應(yīng)用到一個(gè)聲明上,我們得寫一個(gè)裝飾器工廠函數(shù)。 裝飾器工廠就是一個(gè)簡(jiǎn)單的函數(shù),它返回一個(gè)表達(dá)式,以供裝飾器在運(yùn)行時(shí)調(diào)用。

我們可以通過(guò)下面的方式來(lái)寫一個(gè)裝飾器工廠函數(shù):

function color(value: string) { // 這是一個(gè)裝飾器工廠
    return function (target) { //  這是裝飾器
        // do something with "target" and "value"...
    }
}

注意? 下面方法裝飾器小節(jié)里有一個(gè)更加詳細(xì)的例子。

裝飾器組合

多個(gè)裝飾器可以同時(shí)應(yīng)用到一個(gè)聲明上,就像下面的示例:

  • 書寫在同一行上:
@f @g x
  • 書寫在多行上:
@f
@g
x

當(dāng)多個(gè)裝飾器應(yīng)用于一個(gè)聲明上,它們求值方式與復(fù)合函數(shù)相似。在這個(gè)模型下,當(dāng)復(fù)合fg時(shí),復(fù)合的結(jié)果(f ° g)(x)等同于f(g(x))。

同樣的,在TypeScript里,當(dāng)多個(gè)裝飾器應(yīng)用在一個(gè)聲明上時(shí)會(huì)進(jìn)行如下步驟的操作:

  1. 由上至下依次對(duì)裝飾器表達(dá)式求值。
  2. 求值的結(jié)果會(huì)被當(dāng)作函數(shù),由下至上依次調(diào)用。

如果我們使用裝飾器工廠的話,可以通過(guò)下面的例子來(lái)觀察它們求值的順序:

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}

在控制臺(tái)里會(huì)打印出如下結(jié)果:

f(): evaluated
g(): evaluated
g(): called
f(): called

裝飾器求值

類中不同聲明上的裝飾器將按以下規(guī)定的順序應(yīng)用:

  1. 參數(shù)裝飾器,其次是方法,訪問(wèn)符,或屬性裝飾器應(yīng)用到每個(gè)實(shí)例成員。
  2. 參數(shù)裝飾器,其次是方法,訪問(wèn)符,或屬性裝飾器應(yīng)用到每個(gè)靜態(tài)成員。
  3. 參數(shù)裝飾器應(yīng)用到構(gòu)造函數(shù)。
  4. 類裝飾器應(yīng)用到類。

類裝飾器

類裝飾器在類聲明之前被聲明(緊靠著類聲明)。 類裝飾器應(yīng)用于類構(gòu)造函數(shù),可以用來(lái)監(jiān)視,修改或替換類定義。 類裝飾器不能用在聲明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的類)。

類裝飾器表達(dá)式會(huì)在運(yùn)行時(shí)當(dāng)作函數(shù)被調(diào)用,類的構(gòu)造函數(shù)作為其唯一的參數(shù)。

如果類裝飾器返回一個(gè)值,它會(huì)使用提供的構(gòu)造函數(shù)來(lái)替換類的聲明。

注意  如果你要返回一個(gè)新的構(gòu)造函數(shù),你必須注意處理好原來(lái)的原型鏈。 在運(yùn)行時(shí)的裝飾器調(diào)用邏輯中 不會(huì)為你做這些。

下面是使用類裝飾器(@sealed)的例子,應(yīng)用在Greeter類:

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

我們可以這樣定義@sealed裝飾器:

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

當(dāng)@sealed被執(zhí)行的時(shí)候,它將密封此類的構(gòu)造函數(shù)和原型。(注:參見(jiàn)Object.seal)

方法裝飾器

方法裝飾器聲明在一個(gè)方法的聲明之前(緊靠著方法聲明)。 它會(huì)被應(yīng)用到方法的 屬性描述符上,可以用來(lái)監(jiān)視,修改或者替換方法定義。 方法裝飾器不能用在聲明文件( .d.ts),重載或者任何外部上下文(比如declare的類)中。

方法裝飾器表達(dá)式會(huì)在運(yùn)行時(shí)當(dāng)作函數(shù)被調(diào)用,傳入下列3個(gè)參數(shù):

  1. 對(duì)于靜態(tài)成員來(lái)說(shuō)是類的構(gòu)造函數(shù),對(duì)于實(shí)例成員是類的原型對(duì)象。
  2. 成員的名字。
  3. 成員的屬性描述符

注意? 如果代碼輸出目標(biāo)版本小于ES5,Property Descriptor將會(huì)是undefined。

如果方法裝飾器返回一個(gè)值,它會(huì)被用作方法的屬性描述符。

注意? 如果代碼輸出目標(biāo)版本小于ES5返回值會(huì)被忽略。

下面是一個(gè)方法裝飾器(@enumerable)的例子,應(yīng)用于Greeter類的方法上:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

我們可以用下面的函數(shù)聲明來(lái)定義@enumerable裝飾器:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

這里的@enumerable(false)是一個(gè)裝飾器工廠。 當(dāng)裝飾器 @enumerable(false)被調(diào)用時(shí),它會(huì)修改屬性描述符的enumerable屬性。

訪問(wèn)器裝飾器

訪問(wèn)器裝飾器聲明在一個(gè)訪問(wèn)器的聲明之前(緊靠著訪問(wèn)器聲明)。 訪問(wèn)器裝飾器應(yīng)用于訪問(wèn)器的 屬性描述符并且可以用來(lái)監(jiān)視,修改或替換一個(gè)訪問(wèn)器的定義。 訪問(wèn)器裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如 declare的類)里。

注意? TypeScript不允許同時(shí)裝飾一個(gè)成員的getset訪問(wèn)器。取而代之的是,一個(gè)成員的所有裝飾的必須應(yīng)用在文檔順序的第一個(gè)訪問(wèn)器上。這是因?yàn)椋谘b飾器應(yīng)用于一個(gè)屬性描述符時(shí),它聯(lián)合了getset訪問(wèn)器,而不是分開(kāi)聲明的。

訪問(wèn)器裝飾器表達(dá)式會(huì)在運(yùn)行時(shí)當(dāng)作函數(shù)被調(diào)用,傳入下列3個(gè)參數(shù):

  1. 對(duì)于靜態(tài)成員來(lái)說(shuō)是類的構(gòu)造函數(shù),對(duì)于實(shí)例成員是類的原型對(duì)象。
  2. 成員的名字。
  3. 成員的屬性描述符。

注意? 如果代碼輸出目標(biāo)版本小于ES5Property Descriptor將會(huì)是undefined。

如果訪問(wèn)器裝飾器返回一個(gè)值,它會(huì)被用作方法的屬性描述符。

注意? 如果代碼輸出目標(biāo)版本小于ES5返回值會(huì)被忽略。

下面是使用了訪問(wèn)器裝飾器(@configurable)的例子,應(yīng)用于Point類的成員上:

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

我們可以通過(guò)如下函數(shù)聲明來(lái)定義@configurable裝飾器:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

屬性裝飾器

屬性裝飾器聲明在一個(gè)屬性聲明之前(緊靠著屬性聲明)。 屬性裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如 declare的類)里。

屬性裝飾器表達(dá)式會(huì)在運(yùn)行時(shí)當(dāng)作函數(shù)被調(diào)用,傳入下列2個(gè)參數(shù):

  1. 對(duì)于靜態(tài)成員來(lái)說(shuō)是類的構(gòu)造函數(shù),對(duì)于實(shí)例成員是類的原型對(duì)象。
  2. 成員的名字。

注意? 屬性描述符不會(huì)做為參數(shù)傳入屬性裝飾器,這與TypeScript是如何初始化屬性裝飾器的有關(guān)。 因?yàn)槟壳皼](méi)有辦法在定義一個(gè)原型對(duì)象的成員時(shí)描述一個(gè)實(shí)例屬性,并且沒(méi)辦法監(jiān)視或修改一個(gè)屬性的初始化方法。 因此,屬性描述符只能用來(lái)監(jiān)視類中是否聲明了某個(gè)名字的屬性。

如果屬性裝飾器返回一個(gè)值,它會(huì)被用作方法的屬性描述符。

注意? 如果代碼輸出目標(biāo)版本小于ES5,返回值會(huì)被忽略。

如果訪問(wèn)符裝飾器返回一個(gè)值,它會(huì)被用作方法的屬性描述符。

我們可以用它來(lái)記錄這個(gè)屬性的元數(shù)據(jù),如下例所示:

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

然后定義@format裝飾器和getFormat函數(shù):

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

這個(gè)@format("Hello, %s")裝飾器是個(gè) 裝飾器工廠。 當(dāng) @format("Hello, %s")被調(diào)用時(shí),它添加一條這個(gè)屬性的元數(shù)據(jù),通過(guò)reflect-metadata庫(kù)里的Reflect.metadata函數(shù)。 當(dāng) getFormat被調(diào)用時(shí),它讀取格式的元數(shù)據(jù)。

注意? 這個(gè)例子需要使用reflect-metadata庫(kù)。 查看 元數(shù)據(jù)了解reflect-metadata庫(kù)更詳細(xì)的信息。

參數(shù)裝飾器

參數(shù)裝飾器聲明在一個(gè)參數(shù)聲明之前(緊靠著參數(shù)聲明)。 參數(shù)裝飾器應(yīng)用于類構(gòu)造函數(shù)或方法聲明。 參數(shù)裝飾器不能用在聲明文件(.d.ts),重載或其它外部上下文(比如 declare的類)里。

參數(shù)裝飾器表達(dá)式會(huì)在運(yùn)行時(shí)當(dāng)作函數(shù)被調(diào)用,傳入下列3個(gè)參數(shù):

  1. 對(duì)于靜態(tài)成員來(lái)說(shuō)是類的構(gòu)造函數(shù),對(duì)于實(shí)例成員是類的原型對(duì)象。
  2. 成員的名字。
  3. 參數(shù)在函數(shù)參數(shù)列表中的索引。

注意? 參數(shù)裝飾器只能用來(lái)監(jiān)視一個(gè)方法的參數(shù)是否被傳入。

參數(shù)裝飾器的返回值會(huì)被忽略。

下例定義了參數(shù)裝飾器(@required)并應(yīng)用于Greeter類方法的一個(gè)參數(shù):

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

然后我們使用下面的函數(shù)定義 @required 和 @validate 裝飾器:

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}

@required裝飾器添加了元數(shù)據(jù)實(shí)體把參數(shù)標(biāo)記為必需的。 @validate裝飾器把greet方法包裹在一個(gè)函數(shù)里在調(diào)用原先的函數(shù)前驗(yàn)證函數(shù)參數(shù)。

注意? 這個(gè)例子使用了reflect-metadata庫(kù)。 查看 元數(shù)據(jù)了解reflect-metadata庫(kù)的更多信息。

元數(shù)據(jù)

一些例子使用了reflect-metadata庫(kù)來(lái)支持實(shí)驗(yàn)性的metadata API。 這個(gè)庫(kù)還不是ECMAScript (JavaScript)標(biāo)準(zhǔn)的一部分。 然而,當(dāng)裝飾器被ECMAScript官方標(biāo)準(zhǔn)采納后,這些擴(kuò)展也將被推薦給ECMAScript以采納。

你可以通過(guò)npm安裝這個(gè)庫(kù):

npm i reflect-metadata --save

TypeScript支持為帶有裝飾器的聲明生成元數(shù)據(jù)。 你需要在命令行或 tsconfig.json里啟用emitDecoratorMetadata編譯器選項(xiàng)。

Command Line:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

當(dāng)啟用后,只要reflect-metadata庫(kù)被引入了,設(shè)計(jì)階段添加的類型信息可以在運(yùn)行時(shí)使用。

如下例所示:

import "reflect-metadata";

class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
    let set = descriptor.set;
    descriptor.set = function (value: T) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
    }
}

TypeScript編譯器可以通過(guò)@Reflect.metadata裝飾器注入設(shè)計(jì)階段的類型信息。 你可以認(rèn)為它相當(dāng)于下面的TypeScript:

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

注意? 裝飾器元數(shù)據(jù)是個(gè)實(shí)驗(yàn)性的特性并且可能在以后的版本中發(fā)生破壞性的改變(breaking changes)。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)