TypeScript 中裝飾器的理解?應用場景?

 

 

 

一、是什麼

裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明,方法, 訪問符,屬性或參數上

是一種在不改變原類和使用繼承的情況下,動態地擴展對象功能

同樣的,本質也不是什麼高大上的結構,就是一個普通的函數,@expression 的形式其實是Object.defineProperty的語法糖

expression求值後必須也是一個函數,它會在運行時被調用,被裝飾的聲明信息做為參數傳入

二、使用方式

由於typescript是一個實驗性特性,若要使用,需要在tsconfig.json文件啟動,如下:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

 

 

typescript裝飾器的使用和javascript基本一致

類的裝飾器可以裝飾:

  • 方法/屬性

  • 參數

  • 訪問器

類裝飾

例如聲明一個函數 addAge 去給 Class 的屬性 age 添加年齡.

function addAge(constructor: Function) {
  constructor.prototype.age = 18;
}

@addAge
class Person{
  name: string;
  age!: number;
  constructor() {
    this.name = 'huihui';
  }
}

let person = new Person();

console.log(person.age); // 18

 

 

上述代碼,實際等同於以下形式:

Person = addAge(function Person() { ... });

上述可以看到,當裝飾器作為修飾類的時候,會把構造器傳遞進去。constructor.prototype.age 就是在每一個實例化對象上面添加一個 age 屬性

方法/屬性裝飾

同樣,裝飾器可以用於修飾類的方法,這時候裝飾器函數接收的參數變成了:

  • target:對象的原型
  • propertyKey:方法的名稱
  • descriptor:方法的屬性描述符

可以看到,這三個屬性實際就是Object.defineProperty的三個參數,如果是類的屬性,則沒有傳遞第三個參數

如下例子:

// 聲明裝飾器修飾方法/屬性
function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(target);
  console.log("prop " + propertyKey);
  console.log("desc " + JSON.stringify(descriptor) + "\n\n");
  descriptor.writable = false;
};

function property(target: any, propertyKey: string) {
  console.log("target", target)
  console.log("propertyKey", propertyKey)
}

class Person{
 @property
 name: string;
 constructor() {
   this.name = 'huihui';
 }

 @method
 say(){
   return 'instance method';
 }

 @method
 static run(){
   return 'static method';
 }
}

const xmz = new Person();

// 修改實例方法say
xmz.say = function() {
 return 'edit'
}

 

 

輸出如下圖所示:

 

 

 

 

 

參數裝飾

接收3個參數,分別是:

  • target :當前對象的原型
  • propertyKey :參數的名稱
  • index:參數數組中的位置
function logParameter(target: Object, propertyName: string, index: number) {
  console.log(target);
  console.log(propertyName);
  console.log(index);
}

class Employee {
  greet(@logParameter message: string): string {
      return `hello ${message}`;
  }
}
const emp = new Employee();
emp.greet('hello');

 

 

輸入如下圖:

 

 

訪問器裝飾

使用起來方式與方法裝飾一致,如下:

 
function modification(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(target);
  console.log("prop " + propertyKey);
  console.log("desc " + JSON.stringify(descriptor) + "\n\n");
};

class Person{
 _name: string;
 constructor() {
   this._name = 'huihui';
 }

 @modification
 get name() {
   return this._name
 }
}

 

 

裝飾器工廠

如果想要傳遞參數,使裝飾器變成類似工廠函數,只需要在裝飾器函數內部再函數一個函數即可,如下:

function addAge(age: number) {
  return function(constructor: Function) {
    constructor.prototype.age = age
  }
}

@addAge(10)
class Person{
  name: string;
  age!: number;
  constructor() {
    this.name = 'huihui';
  }
}

let person = new Person();

 

 

執行順序

當多個裝飾器應用於一個聲明上,將由上至下依次對裝飾器表達式求值,求值的結果會被當作函數,由下至上依次調用,例如如下:

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}

// 輸出
f(): evaluated
g(): evaluated
g(): called
f(): called

 

 

三、應用場景

可以看到,使用裝飾器存在兩個顯著的優點:

  • 代碼可讀性變強了,裝飾器命名相當於一個注釋
  • 在不改變原有代碼情況下,對原來功能進行擴展

後面的使用場景中,藉助裝飾器的特性,除了提高可讀性之後,針對已經存在的類,可以通過裝飾器的特性,在不改變原有代碼情況下,對原來功能進行擴展