在我們深入瞭解 Angular 2 中 @NgModule、@Component、@Injectable 等常見的裝飾器之前,我們要先了解 TypeScript 中的裝飾器。裝飾器是一個非常酷的特性,最早出現在 Google 的 AtScript 中,它出現的目的是為了讓開發者,開發出更容易維護、更容易理解的 Angular 程式碼。令人興奮的是,在2015年 Angular 團隊跟 MicroSoft 的 TypeScript 團隊經過數月的的交流,最終決定採用 TypeScript 來重寫 Angular 2 專案 。
裝飾器是什麼
-
它是一個表示式
-
該表示式被執行後,返回一個函式
-
函式的入參分別為 targe、name 和 descriptor
-
執行該函式後,可能返回 descriptor 物件,用於配置 target 物件
裝飾器的分類
-
類裝飾器 (Class decorators)
-
屬性裝飾器 (Property decorators)
-
方法裝飾器 (Method decorators)
-
引數裝飾器 (Parameter decorators)
TypeScript 類裝飾器
類裝飾器宣告:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void
類裝飾器顧名思義,就是用來裝飾類的。它接收一個引數:
-
target: TFunction – 被裝飾的類
看完第一眼後,是不是感覺都不好了。沒事,我們馬上來個例子:
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log(`Hello!`);
}
}
@Greeter
class Greeting {
constructor() {
// 內部實現
}
}
let myGreeting = new Greeting();
myGreeting.greet(); // console output: `Hello!`;
上面的例子中,我們定義了 Greeter 類裝飾器,同時我們使用了 @Greeter 新的語法,來使用裝飾器。
(備註:讀者可以直接複製上面的程式碼,在 TypeScript Playground 中執行檢視結果)。
有的讀者可能想問,例子中總是輸出 Hello! ,能自定義輸出的問候語麼 ?這個問題很好,答案是可以的。具體實現如下:
function Greeter(greeting: string) {
return function(target: Function) {
target.prototype.greet = function(): void {
console.log(greeting);
}
}
}
@Greeter(`您好`)
class Greeting {
constructor() {
// 內部實現
}
}
let myGreeting = new Greeting();
myGreeting.greet(); // console output: `您好!`;
TypeScript 屬性裝飾器
屬性裝飾器宣告:
declare type PropertyDecorator = (target:Object, propertyKey: string | symbol ) => void;
屬性裝飾器顧名思義,用來裝飾類的屬性。它接收兩個引數:
-
target: Object – 被裝飾的類
-
propertyKey:string | symbol – 被裝飾類的屬性名
趁熱打鐵,馬上來個例子熱熱身:
function LogChanges(target: Object, key: string) {
var propertyValue: string = this[key];
if(delete this[key]) {
Object.defineProperty(target, key, {
get: function () {
return propertyValue;
},
set: function(newValue) {
propertyValue = newValue;
console.log(`${key} is now ${propertyValue}`);
}
});
}
}
class Fruit {
@LogChanges
name: string;
}
let fruit = new Fruit();
fruit.name = `apple`; // console output: `name is now apple`
fruit.name = `banana`; // console output: `name is now banana`
那麼問題來了,如果使用者想在屬性變化的時候,自動重新整理頁面,而不是簡單地在控制檯輸出訊息,那要怎麼辦?我們能不能參照類裝飾器自定義問候語的方式,來實現監測屬性變化的功能。具體實現如下:
function LogChanges(callbackObject: any) {
return function(target: Object, key: string): void {
var propertyValue: string = this[key];
if(delete this[key]) {
Object.defineProperty(target, key, {
get: function () {
return propertyValue;
},
set: function(newValue) {
propertyValue = newValue;
callbackObject.onchange.call(this, propertyValue);
}
});
}
}
}
class Fruit {
@LogChanges({
onchange: function(newValue: string): void {
console.log(`The fruit is ${newValue} now`);
}
})
name: string;
}
let fruit = new Fruit();
fruit.name = `apple`; // console output: `The fruit is apple now`
fruit.name = `banana`; // console output: `The fruit is banana now`
TypeScript 方法裝飾器
方法裝飾器宣告:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
方法裝飾器顧名思義,用來裝飾類的屬性。它接收三個引數:
-
target: Object – 被裝飾的類
-
propertyKey: string | symbol – 方法名
-
descriptor: TypePropertyDescript – 屬性描述符
廢話不多說,直接上例子:
function LogOutput(tarage: Function, key: string, descriptor: any) {
var originalMethod = descriptor.value;
var newMethod = function(...args: any[]): any {
var result: any = originalMethod.apply(this, args);
if(!this.loggedOutput) {
this.loggedOutput = new Array<any>();
}
this.loggedOutput.push({
method: key,
parameters: args,
output: result,
timestamp: new Date()
});
return result;
};
descriptor.value = newMethod;
}
class Calculator {
@LogOutput
double (num: number): number {
return num * 2;
}
}
let calc = new Calculator();
calc.double(11);
// console ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput);
最後我們來看一下引數裝飾器:
TypeScript 引數裝飾器
引數裝飾器宣告:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void
引數裝飾器顧名思義,是用來裝飾函式引數,它接收三個引數:
-
target: Object – 被裝飾的類
-
propertyKey: string | symbol – 方法名
-
parameterIndex: number – 方法中引數的索引值
function Log(target: Function, key: string, parameterIndex: number) {
var functionLogged = key || target.prototype.constructor.name;
console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
}
class Greeter {
greeting: string;
constructor(@Log phrase: string) {
this.greeting = phrase;
}
}
// console output: The parameter in position 0 at Greeter has
// been decorated
我有話說
1.Object.defineProperty() 方法有什麼用 ?
Object.defineProperty 用於在一個物件上定義一個新的屬性或者修改一個已存在的屬性,並返回這個物件。 方法的簽名:Object.defineProperty(obj, prop, descriptor) ,引數說明如下:
-
obj 需要定義的屬性物件
-
prop 需被定義或修改的屬性名
-
descriptor 需被定義或修改的屬性的描述符
物件裡目前存在的屬性描述符有兩種主要形式:資料描述符和存取描述符。資料描述符是一個擁有可寫或不可寫值的屬性。存取描述符是由一對 getter-setter 函式功能來描述的屬性。描述符必須是兩種形式之一,不能同時是兩者。
資料描述符和存取描述符均具有以下可選鍵值:
-
configurable
當且僅當該屬性的 configurable 為 true 時,該屬性才能夠被改變,也能夠被刪除。預設為 false。 -
enumerable
當且僅當該屬性的 enumerable 為 true 時,該屬性才能夠出現在物件的列舉屬性中。預設為 false。
資料描述符同時具有以下可選鍵值:
-
value
該屬性對應的值。可以是任何有效的 JavaScript 值(數值,物件,函式等)。預設為 undefined。 -
writable
當且僅當僅當該屬性的writable為 true 時,該屬性才能被賦值運算子改變。預設為 false。
存取描述符同時具有以下可選鍵值:
-
get
一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。預設為undefined。 -
set
一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一引數,並將該引數的新值分配給該屬性。預設為undefined。
使用示例:
var o = {}; // 建立一個新物件
Object.defineProperty(o, "a", {value : 37, writable : true, enumerable : true,
configurable : true});
總結
本文主要介紹了 TypeScript 中的四種裝飾器,瞭解裝飾器的基本分類和實現原理,為我們下一篇深入 Angular 2 的 @NgModule、@Component、@Injectable 等常用裝飾器做好鋪墊。