一、裝飾器的作用

我個人的理解是:ts中的裝飾器類似於 Java 語言中的註解,對於使用者來說都是為類和屬性等程式碼元素新增額外的功能,而不改變程式碼元素原有的結構。例如在 Java 中我們用的比較多的 Spring 框架中的註解 @Component 可以將一個類放置到 IoC 容器中進行託管,使用 @Autowired 來取出該物件進行使用等等。

二、類裝飾器

1. 普通裝飾器

先寫一個最簡單的類裝飾器,它看起來就是一個方法:

function logClass(params:any) {
console.log(params);
}

使用方式如下,非常類似Java的註解:

@裝飾器名
class 被裝飾的類 {
...
}

然後我們編寫一個類來進行裝飾

@logClass // <-- 類裝飾器的使用方式
class HttpClient {
constructor() {
}
getData() {
}
}

檢視執行的結果:

可以看出,logClass 引數 params 中得到的是修飾的類(在console中打印出型別是方法的原因是ts中的在編譯成js程式碼後,會變成一個函式

為類擴充套件屬性和方法

擴充套件屬性

這裡為被修飾的類新增一個 info 屬性:

function logClass(params:any) {
// 在原型鏈上新增附加屬性
params.prototype.info = '附加資訊'
}

然後我們將這個屬性打印出來:

let httpClient:any = new HttpClient();
console.log(httpClient.info);

執行結果:

tips:注意這裡的建立的物件的型別需要指定為 any 才可以使用 info 屬性

擴充套件方法

同理,我們也可以為被修飾的類擴充套件方法:

function logClass(params:any) {
params.prototype.run = function() {
console.log("我是擴充套件的run方法");
}
}

方法呼叫:

let httpClient:any = new HttpClient();
httpClient.run();

執行結果:

使用裝飾器修改屬性和重寫方法

語法:

function 裝飾器名(target:any) {
return class extends target {
原有的某屬性:any = "我是修改後的資料";
原有的某方法() {
//...重寫的內容
}
}
}

示例:

function logClass(target:any) {
return class extends target {
apiUrl:any = "我是修改後的資料";
getData() {
this.apiUrl += "-----";
console.log(this.apiUrl);
}
}
}

被修飾的類:

@logClass
class HttpClient {
public apiUrl: string | undefined;
constructor() {
}
getData() {
}
}

執行程式碼:

let httpClient:HttpClient = new HttpClient();
httpClient.getData();

執行結果:

可以看到,使用裝飾器,我們不需要編寫子類即可修改類的屬性重寫類的方法了!

2. 裝飾器工廠

當我們使用普通裝飾器時可以發現,我們是無法傳遞引數的,而現在要學的裝飾器工廠則可以傳遞引數。

語法格式:

function 裝飾器名(引數列表) {
return function(target: any) {
...
}
}

現在引數列表中的是我們輸入的引數,而 target 是被裝飾的類。

示例:

function logClass(params:string, p2?:string) {
return function(target: any) {
console.log("params",params);
console.log("p2",p2);
console.log("target",target);
}
}

被裝飾的類:

@logClass("hello world", "第二個引數")
class HttpClient {
constructor() {
}
getData() {
}
}

執行結果:

三、屬性裝飾器

定義方式:

function 屬性裝飾器名() {
return function(target: any, attr: any) {
...
}
}

這裡返回的 function 有兩個引數:

  1. 對於靜態成員來說是類的建構函式,對於例項成員來說是類的原型物件
  2. 成員的名字(string型別)

例如我們可以使用下面的方式使用屬性裝飾器修改屬性

屬性裝飾器:

function logProperty(params: any) {
return function(target: any, attr: any) {
target[attr] = params;
}
}

被裝飾的類:

// @logClass
class HttpClient {
@logProperty("hello world")
public apiUrl: string | undefined;
constructor() {
}
getData() {
}
}

列印屬性:

let httpClient:HttpClient = new HttpClient();
console.log(httpClient.apiUrl);

執行結果:

可以看到屬性的值被修改為了我們傳入到註解中的值了。

四、方法裝飾器

定義方式:

function 裝飾器名(引數列表) {
return function(target:any, methodName:any, desc:any) {
...
}
}

這裡返回的 function 有三個引數:

  1. target 是修飾的類的物件原型
  2. methodName 是方法名
  3. desc 方法的描述資訊物件

我們可以將它們打印出來:

function Get(url:string) {
return function(target:any, methodName:any, desc:any) {
console.log(target);
console.log(methodName);
console.log(desc);
}
}
class HttpClient {
constructor() {
}
@Get("http://www.itying.com")
getData() {
}
}

執行結果:

使用方法裝飾器對方法進行擴充套件

被修飾類:

class HttpClient {
constructor() {
}
getData(...args:any[]) {
console.log(args);
}
}

此時我們希望擴充套件 getData() 方法的功能:在執行前後進行日誌列印,並將傳入的引數先轉換為 string 型別。

具體的裝飾器如下:

function logMethod() {
return function(target:any, methodName:any, desc:any) {
// 先儲存原來的函式
let originMethod:Function = desc.value;
// 修改函式的實現
desc.value = function(...args:any[]) {
// 資料預處理(轉為string)
args = args.map(value=>String(value));
// 執行前日誌
console.log(`====函式${methodName}執行前====`);
// 呼叫原函式
originMethod.apply(this, args);
// 執行後日志
console.log(`====函式${methodName}執行後====`);
}
}
}

建立物件並呼叫方法:

let httpClient:HttpClient = new HttpClient();
httpClient.getData(1,2,3);

執行結果:

五、方法引數裝飾器

定義方法:

function logParam(){
return function(target:any, methodName:any,paramIndex:any) {
...
}
}

定義方法和方法裝飾器基本一致,區別在於三個引數不同:

  1. target 是修飾的類的物件原型
  2. methodName 是方法名
  3. paramIndex 是引數的索引

示例:

function logParam(){
return function(target:any, methodName:any,paramIndex:any) {
console.log(target);
console.log(methodName);
console.log(paramIndex);
}
}

被修飾的類

class HttpClient {
// @logProperty("hello world")
public apiUrl: string | undefined;
constructor() {
}
getData(@logParam() str:string) {
console.log("getData函式被呼叫了");
}
}

執行結果:

六、裝飾器的執行順序

屬性裝飾器 => 方法裝飾器 => 方法引數裝飾器 => 類裝飾器