Typescript 的高級語法之類裝飾器、方法裝飾器、訪問器裝飾器、屬性裝飾器和參數裝飾器

一、Typescript 的類裝飾器和方法裝飾器

  1. 隨着 TypeScriptES6 裏引入了類,在一些場景下我們需要額外的特性來支持標註或修改類及其成員。 裝飾器(Decorators)爲我們在類的聲明及成員上通過元編程語法添加標註提供了一種方式。 裝飾器是一項實驗性特性,在未來的版本中可能會發生改變。若要啓用實驗性的裝飾器特性,你必須在 tsconfig.json 文件中進行配置開啓,代碼如下所示:
    /* Experimental Options */
    "experimentalDecorators": true,
    /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true,
    /* Enables experimental support for emitting type metadata for decorators. */
  1. 裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明,方法, 訪問符,屬性或參數上。 裝飾器使用 @expression 這種形式,expression 求值後必須爲一個函數,它會在運行時被調用,被裝飾的聲明信息做爲參數傳入。裝飾器工廠就是一個簡單的函數,它返回一個表達式,以供裝飾器在運行時調用。在 TypeScript 裏,當多個裝飾器應用在一個聲明上時,由上至下依次對裝飾器表達式求值,求值的結果會被當作函數,由下至上依次調用。
  2. 裝飾器本身是一個函數,類裝飾器接受的參數是構造函數,裝飾器通過 @ 符號來使用。類被創建的時候,裝飾器就會執行,對類進行修飾,多個類裝飾器,從右到左、從下到上 依次執行,先收集的裝飾器會後執行。對於工廠模式的類裝飾器,函數執行後,這個函數的返回值就會作爲裝飾器。如下,創建 testDecorator 裝飾器,接收的構造函數中,創建 getName 這個方法,通過 @testDecorator 去使用這個構造器。對於多個裝飾器,從下到上執行,@testDecorator2 先執行,@testDecorator 後執行,代碼如下所示:
function testDecorator(constructor: any) {

  constructor.prototype.getName = () => {
    console.log("Tom");
  };

}

function testDecorator2(constructor: any) {
  console.log("decorator2");
}

@testDecorator
@testDecorator2
class Test2 {}

const test = new Test2();
(test as any).getName();

function testDecorator3(flag: boolean) {
  if (flag) {
    return function testDecorator(constructor: any) {
      constructor.prototype.getName = () => {
        console.log("Tom");
      };
    };
  } else {
    return function (constructor: any) {};
  }
}

@testDecorator3(true)
class Test3 {}

  1. 裝飾器求值,類中不同聲明上的裝飾器將按以下規定的順序應用,如下所示:
  • 參數裝飾器,然後依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應用到每個實例成員
  • 參數裝飾器,然後依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應用到每個靜態成員
  • 參數裝飾器應用到構造函數
  • 類裝飾器應用到類
  1. 類裝飾器在類聲明之前被聲明(緊靠着類聲明)。 類裝飾器應用於類構造函數,可以用來監視,修改或替換類定義。 類裝飾器不能用在聲明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的類)。類裝飾器表達式會在運行時當作函數被調用,類的構造函數作爲其唯一的參數。如果類裝飾器返回一個值,它會使用提供的構造函數來替換類的聲明。如果你要返回一個新的構造函數,你必須注意處理好原來的原型鏈。 在運行時的裝飾器調用邏輯中 不會爲你做這些。
  2. 一個構造函數,可以接收很多的參數,合併到一起就是數組,每一個參數都是any 類型,最終的返回值也是 any 類型。 T 這個泛型可以被這個構造函數所實例化出來,類包含這個構造函數。 類的裝飾器,也可以對構造函數進行擴展。舊的構造函數先執行,最後再執行裝飾器的函數,代碼如下所示:
function testDecorator() {
  return function <T extends new (...args: any[]) => any>(constructor: T) {
    return class extends constructor {
      name = "Tom";
      getName() {
        return this.name;
      }
    };
  };
}

// @testDecorator
// class Test2 {
//   name: string;
//   constructor(name: string) {
//     this.name = name;
//   }
// }
const Test = testDecorator()(
  class {
    name: string;
    constructor(name: string) {
      this.name = name;
    }
  }
);

const test = new Test("Jack");
console.log(test.getName());

  1. 方法裝飾器聲明在一個方法的聲明之前(緊靠着方法聲明)。 它會被應用到方法的 屬性描述符上,可以用來監視,修改或者替換方法定義。 方法裝飾器不能用在聲明文件( .d.ts),重載或者任何外部上下文(比如declare的類)中。
  2. 如果方法裝飾器返回一個值,它會被用作方法的屬性描述符。方法裝飾器表達式會在運行時當作函數被調用,傳入下列3個參數,如下所示:
  • 對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象
  • 成員的名字
  • 成員的屬性描述符
  1. 對類中的方法進行裝飾器,對普通方法,target 對應的是類的 prototype。對靜態方法,target 對應的是類的構造函數。方法裝飾器可以對原型、對 key 值,對 descriptor 都可以進行修改,如下所示:
function getNameDecorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  console.log(target, key);
  descriptor.writable = true;
  descriptor.value = function () {
    return "descriptor";
  };
}

class Test {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  @getNameDecorator
  getName() {
    return this.name;
  }
}

const test = new Test("Tom");
console.log(test.getName());

二、Typescript 的訪問器裝飾器、屬性裝飾器和參數裝飾器

  1. 訪問器裝飾器聲明在一個訪問器的聲明之前(緊靠着訪問器聲明)。 訪問器裝飾器應用於訪問器的 屬性描述符並且可以用來監視,修改或替換一個訪問器的定義。 訪問器裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如 declare的類)裏。 TypeScript 不允許同時裝飾一個成員的 getset 訪問器。取而代之的是,一個成員的所有裝飾的必須應用在文檔順序的第一個訪問器上。這是因爲,在裝飾器應用於一個屬性描述符時,它聯合了getset訪問器,而不是分開聲明的。
  2. 如果訪問器裝飾器返回一個值,它會被用作方法的屬性描述符。訪問器裝飾器表達式會在運行時當作函數被調用,傳入下列 3 個參數,如下所示:
  • 對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象
  • 成員的名字
  • 成員的屬性描述符
  1. 訪問器裝飾器,在 set 上進行裝飾器的修改,代碼如下所示:
function visitDecorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  descriptor.writable = false;
}

class Test {
  private _name: string;
  constructor(name: string) {
    this._name = name;
  }
  get name() {
    return this._name;
  }
  @visitDecorator
  set name(name: string) {
    this._name = name;
  }
}

const test = new Test("Tom");
test.name = "zhangsan";
console.log(test.name);

  1. 屬性裝飾器聲明在一個屬性聲明之前(緊靠着屬性聲明)。 屬性裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如 declare的類)裏。屬性描述符不會做爲參數傳入屬性裝飾器,這與 TypeScript 是如何初始化屬性裝飾器的有關。 因爲目前沒有辦法在定義一個原型對象的成員時描述一個實例屬性,並且沒辦法監視或修改一個屬性的初始化方法。返回值也會被忽略。因此,屬性描述符只能用來監視類中是否聲明瞭某個名字的屬性。屬性裝飾器表達式會在運行時當作函數被調用,傳入下列 2 個參數,如下所示:
  • 對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象
  • 成員的名字
  1. 屬性裝飾器,可以修改屬性的 descriptor 的值。性裝飾器的寫法也是 Decorator 的寫法,接收兩個參數,分別是原型和屬性的名字,可以返回 一個 descriptor 替換掉屬性原始的 descriptor。使用屬性裝飾器不能直接修改屬性上的值,實際上修改的是原型上的屬性的值,但是這個屬性是直接存儲在類聲明的實例上的。修改原型上的,並不會對修改實例上的有什麼變更,沒辦法直接修改屬性值。如下,修改的並不是實例上的 name, 而是原型上的 namename 放在實例上,代碼如下所示:

// function nameDecorator(target: any, key: string): any {
//   const descriptor: PropertyDecorator = {
//     writable: false,
//   };
//   return descriptor;
// }

function nameDecorator(target: any, key: string): any {
  target[key] = "Jack";
}

class Test {
  @nameDecorator
  name = "Jack";
}

const test = new Test();
console.log((test as any)._proto_.name);

  1. 參數裝飾器聲明在一個參數聲明之前(緊靠着參數聲明)。 參數裝飾器應用於類構造函數或方法聲明。 參數裝飾器不能用在聲明文件(.d.ts),重載或其它外部上下文(比如 declare的類)裏。參數裝飾器的返回值會被忽略。參數裝飾器只能用來監視一個方法的參數是否被傳入。參數裝飾器表達式會在運行時當作函數被調用 ,傳入下列 3 個參數,如下:
  • 對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象
  • 成員的名字
  • 參數在函數參數列表中的索引
  1. 參數裝飾器,代碼如下所示:

function paramDecorator(target: any, method: string, paramIndex: number) {
  console.log(target, method, paramIndex);
}

class Test {
  getInfo(name: string, @paramDecorator age: number) {
    console.log(name, age);
  }
}

const test = new Test();
test.getInfo("Jack", 30);

  1. 方法裝飾器的應用,進行方法的異常捕獲,代碼如下所示:
const userInfo: any = undefined;

function catchError(msg: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value;
    descriptor.value = function () {
      try {
        fn();
      } catch (e) {
        console.log(msg);
      }
    };
  };
}

class Test {
  @catchError("userInfo.name 不存在")
  getName() {
    return userInfo.name;
  }
  @catchError("userInfo.age 不存在")
  getAge() {
    return userInfo.age;
  }
  @catchError("userInfo.gender 不存在")
  getGender() {
    return userInfo.gender;
  }
}

const test = new Test();
test.getName();
test.getAge();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章