裝飾器(Decorator)是一種程式設計模式(Design Pattern),在某些情況下可以用來替代繼承(inheritance),以更靈活、輕量的方式重用程式碼。



裝飾器其實就是Wrapper的一種應用,舉例來說:

class C {
    A() {
        console.log("A");
    }

    B() {
        console.log("B");
    }
}

class D {
    c: C;

    constructor(c: C) {
        this.c = c;
    }

    A() {
        this.c.A();

        console.log("Z");
    }
}

以上程式中,D的物件實體可以包裹一個C的物件實體,並且擴充C的物件實體的A方法的功能(原本只輸出A,變成輸出AZ)。此類別D就是一個裝飾器,它的用法如下:

const d = new D(new C());

d.A();

TypeScript的裝飾器

TypeScript內建裝飾器的支援,可以用比較易讀的語法來套用裝飾器設計模式。不過這個功能還在實驗中,如果要使用,必須要在tsconfig.json檔案中的compilerOptions欄位內,加上experimentalDecorators欄位,並將欄位值設為true

裝飾器

TypeScript的裝飾器為一個函數,這個函數的型別會根據其要裝飾的對象類型而有所不同。以下會依照裝飾器的裝飾對象來介紹裝飾器。

類別裝飾器

類別裝飾器的函數型別為(target: any) => voidtarget參數即為裝飾對象的建構子函數(即類別本身)。

例如以下函數,就是一個類別裝飾器:

function d(target: any) {
    console.log('Class Decorator');
}

我們可以用小老鼠符號@再接上裝飾器名稱(即函數名稱或儲存函數的變數、參數、常數的名稱)或是路徑,來套用該裝飾器。

類別裝飾器的@要放置於class關鍵字之前。如果也有使用export關鍵字,要放在export關鍵字之前。

例如:

function d(target: any) {
    console.log("Class Decorator");
}

@d
class C {

}

編譯以上程式並立刻執行的話,會輸出Class Decorator文字。

方法裝飾器

若TypeScript的編譯目標為ES5或是以上的版本,方法裝飾器的函數型別為(target: any, propertyKey: string, descriptor: PropertyDescriptor) => void(target: any, propertyKey: string) => voidtarget參數儲存要用來裝飾的類別建構子(裝飾靜態方法時),或是該類別的原型(裝飾物件方法時)。propertyKey參數儲存要用來裝飾的方法名稱,descriptor參數則是儲存屬性的「選項」(例如在先前章節有稍微提到的enumerable)。

例如以下函數,就是一個方法裝飾器:

function d(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Method Decorator");
}

若TypeScript的編譯目標為ES3,則方法裝飾器的函數型別只能為(target: any, propertyKey: string) => void

方法裝飾器的@要放置於方法名稱之前。如果也有使用privateprotectedsetget關鍵字,要放在它們之前。

例如:

function d(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Method Decorator");
}

class C {
    @d
    m() {

    }
}

編譯以上程式並立刻執行的話,會輸出Method Decorator文字。

descriptor參數的value欄位即為方法本身。例如上面看過的這個例子:

class C {
    A() {
        console.log("A");
    }

    B() {
        console.log("B");
    }
}

class D {
    c: C;

    constructor(c: C) {
        this.c = c;
    }

    A() {
        this.c.A();

        console.log("Z");
    }
}

可以將「方法執行完之後輸出一個Z文字」的功能做成TypeScript的裝飾器,將程式改寫如下:

function Z(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function () {
        originalMethod.call(this);

        console.log("Z");
    };
}

class C {
    @Z
    A() {
        console.log("A");
    }

    B() {
        console.log("B");
    }
}

這邊要注意的是,若要在descriptor參數的value欄位中呼叫原本的方法,最好用方法提供的call函數並代入this作為參數來呼叫,這樣才可以確保在原先方法中使用到的this關鍵字會是該物件本身的參考。

參數裝飾器

參數裝飾器的函數型別為(target: any, propertyKey: string, parameterIndex: number) => voidtarget參數儲存要用來裝飾的類別建構子(裝飾靜態方法的參數或是建構子的參數時),或是該類別的原型(裝飾物件方法的參數時)。propertyKey參數儲存要用來裝飾的參數所在的方法名稱。parameterIndex儲存要用來裝飾的參數序數(從0開始數)。

例如以下函數,就是一個參數裝飾器:

function d(target: any, propertyKey: string, parameterIndex: number) {
    console.log("Parameter Decorator");
}

參數裝飾器的@要放置於參數名稱之前。如果也有使用public關鍵字,要放在它之前。

例如:

function d(target: any, propertyKey: string, parameterIndex: number) {
    console.log("Parameter Decorator");
}

class C {
    constructor(@d public a: number) {

    }

    m(@d a: number) {
        this.a = a;
    }
}

編譯以上程式並立刻執行的話,會輸出兩行Parameter Decorator文字。

欄位裝飾器

欄位裝飾器的函數型別為(target: any, propertyKey: string) => voidtarget參數儲存要用來裝飾的類別建構子(裝飾靜態欄位時),或是該類別的原型(裝飾物件欄位時)。propertyKey參數儲存要用來裝飾的欄位名稱。

例如以下函數,就是一個欄位裝飾器:

function d(target: any, propertyKey: string) {
    console.log("Field Decorator");
}

欄位裝飾器的@要放置於欄位名稱之前。如果也有使用privateprotectedreadonly關鍵字,要放在它們之前。

例如:

function d(target: any, propertyKey: string) {
    console.log("Field Decorator");
}

class C {
    @d
    n = 0;
}

編譯以上程式並立刻執行的話,會輸出Field Decorator文字。

裝飾器工廠(Decorator Factory)

我們可以再撰寫一層擁有自訂參數的函數,用它來產生裝飾器,這個函數即稱為「裝飾器工廠」。裝飾器工廠函數的型別並沒有限制,但它的回傳值型別必須要是上面提到的那些裝飾器的型別。

例如以下函數,就是一個類別裝飾器工廠:

function f() {
    console.log("Class Decorator Factory");

    return (target: any) => {
        console.log("Class Decorator");
    };
}

我們可以用小老鼠符號@接上裝飾器工廠名稱或是路徑,再用小括號()傳入要代進裝飾器工廠的參數,來套用該裝飾器工廠。

例如:

function f() {
    console.log("Class Decorator Factory");

    return (target: any) => {
        console.log("Class Decorator");
    };
}

@f()
class C {

}

編譯以上程式並立刻執行的話,會輸出Class Decorator FactoryClass Decorator文字。

再舉個例子,利用裝飾器工廠,我們可以替方法快速加上Log功能,來紀錄它們何時被呼叫的。程式碼如下:

function log(tag: string) {
    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        const originalMethod = descriptor.value;

        descriptor.value = function () {
            console.log(tag);

            originalMethod.call(this);
        };
    };
}


class C {
    @log("m1")
    m1() {

    }

    @log("m2")
    m2() {

    }

    @log("m3")
    m3() {

    }
}

裝飾器的順序

不同對象的裝飾器(或裝飾器工廠)的套用順序是從上到下,從內到外,從右到左。相同對象的裝飾器的套用順序是從下到上(或者說從右到左)例如:

function cd1(target: any) {
    console.log("Class Decorator 1");
}

function cd2(target: any) {
    console.log("Class Decorator 1");
}

function md1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Method Decorator 1");
}

function md2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Method Decorator 2");
}

function md3(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Method Decorator 3");
}

function pd1(target: any, propertyKey: string, parameterIndex: number) {
    console.log("Parameter Decorator 1");
}

function pd2(target: any, propertyKey: string, parameterIndex: number) {
    console.log("Parameter Decorator 2");
}

function fd1(target: any, propertyKey: string) {
    console.log("Field Decorator 1");
}

function fd2(target: any, propertyKey: string) {
    console.log("Field Decorator 2");
}

function fd3(target: any, propertyKey: string) {
    console.log("Field Decorator 3");
}

function fd4(target: any, propertyKey: string) {
    console.log("Field Decorator 4");
}

@cd1
class A {
    @fd1
    @fd2
    m = 0;

    @fd3
    n = 0;
}

@cd2
class B {
    @md1
    @md2
    run() {

    }

    @fd4
    f = 0;

    @md3
    compute(@pd1 x: number, @pd2 y: number) {

    }
}

以上程式的輸出結果如下:

Field Decorator 2
Field Decorator 1
Field Decorator 3
Class Decorator 1
Method Decorator 2
Method Decorator 1
Field Decorator 4
Parameter Decorator 2
Parameter Decorator 1
Method Decorator 3
Class Decorator 1

總結

在這個章節中我們學會了TypeScript的裝飾器用法,利用裝飾器我們可以省下撰寫很多重複程式碼的功夫。最後再舉個模擬HTTP路由程式的例子,如下:

type method = "get" | "post";

const routes: {
    [_: string]: {
        get?: Function,
        post?: Function
    }
} = {};

function resolve(method: method, path: string, ...data: any[]): any {
    if (routes.hasOwnProperty(path)) {
        const handlers = routes[path];

        const handler = handlers[method];

        if (typeof handler === "function") {
            switch (method) {
                case "get":
                    console.log(handler());
                    break;
                case "post":
                    console.log(handler(...data));
                    break;
            }
        } else {
            throw Error(`The path \`${path}\` has no handler for the \`${method}\` method.`);
        }
    } else {
        throw Error(`The path \`${path}\` is undefined.`);
    }
}

function get(path: string) {
    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        if (!routes.hasOwnProperty(path)) {
            routes[path] = {};
        }

        routes[path]["get"] = descriptor.value;
    };
}

function post(path: string) {
    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        if (!routes.hasOwnProperty(path)) {
            routes[path] = {};
        }

        routes[path]["post"] = descriptor.value;
    };
}

class RouteClass {
    @get("/a")
    @post("/a")
    someGetMethod() {
        return "Hello, world!";
    }

    @post("/b")
    somePostMethod(x: number, y: number) {
        return x + y;
    }
}

resolve("get", "/a");
resolve("post", "/a");
resolve("post", "/b", 1, 2);

以上程式的輸出結果如下:

Hello, world!
Hello, world!
3

這個系列的文章就到這裡為止了,對於習慣使用「靜態型別」的程式語言的開發者來說,TypeScript無非是開發「動態型別」的JavaScript程式的利器!