裝飾器(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
,變成輸出A
和Z
)。此類別D
就是一個裝飾器,它的用法如下:
const d = new D(new C());
d.A();
TypeScript的裝飾器
TypeScript內建裝飾器的支援,可以用比較易讀的語法來套用裝飾器設計模式。不過這個功能還在實驗中,如果要使用,必須要在tsconfig.json
檔案中的compilerOptions
欄位內,加上experimentalDecorators
欄位,並將欄位值設為true
。
裝飾器
TypeScript的裝飾器為一個函數,這個函數的型別會根據其要裝飾的對象類型而有所不同。以下會依照裝飾器的裝飾對象來介紹裝飾器。
類別裝飾器
類別裝飾器的函數型別為(target: any) => void
,target
參數即為裝飾對象的建構子函數(即類別本身)。
例如以下函數,就是一個類別裝飾器:
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) => void
。target
參數儲存要用來裝飾的類別建構子(裝飾靜態方法時),或是該類別的原型(裝飾物件方法時)。propertyKey
參數儲存要用來裝飾的方法名稱,descriptor
參數則是儲存屬性的「選項」(例如在先前章節有稍微提到的enumerable
)。
例如以下函數,就是一個方法裝飾器:
function d(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("Method Decorator");
}
若TypeScript的編譯目標為ES3
,則方法裝飾器的函數型別只能為(target: any, propertyKey: string) => void
。
方法裝飾器的@
要放置於方法名稱之前。如果也有使用private
、protected
、set
、get
關鍵字,要放在它們之前。
例如:
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) => void
。target
參數儲存要用來裝飾的類別建構子(裝飾靜態方法的參數或是建構子的參數時),或是該類別的原型(裝飾物件方法的參數時)。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) => void
。target
參數儲存要用來裝飾的類別建構子(裝飾靜態欄位時),或是該類別的原型(裝飾物件欄位時)。propertyKey
參數儲存要用來裝飾的欄位名稱。
例如以下函數,就是一個欄位裝飾器:
function d(target: any, propertyKey: string) {
console.log("Field Decorator");
}
欄位裝飾器的@
要放置於欄位名稱之前。如果也有使用private
、protected
、readonly
關鍵字,要放在它們之前。
例如:
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 Factory
和Class 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 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!
3
這個系列的文章就到這裡為止了,對於習慣使用「靜態型別」的程式語言的開發者來說,TypeScript無非是開發「動態型別」的JavaScript程式的利器!