iT邦幫忙

2023 iThome 鐵人賽

DAY 28
0

裝飾器可以使我們可以在類別、方法、屬性或參數上添加元數據(metadata),並根據這些元數據來自動轉化或擴充程式碼。它可以在類別或方法不修改程式碼的情況下做一些特定的處理,同時也把可以重複使用的邏輯拆分出去。

因為裝飾器在 JavaScript 裡仍然為實驗性語法,所以在 TypeScript 裡使用裝飾器之前,要先在 tsconfig.json 加入 experimentalDecorators 為 true,這樣裝飾器才可以正常運作,不然會報錯。

// tsconfig.json

{
  "compilerOptions": {
    // ...
    "experimentalDecorators": true,
    // "emitDecoratorMetadata": true,
    // ...
  }
}

注意:如果需要想要使用一些實驗性的 metadata API ( ex: reflect-metadata ),因為它還未成為 JavaScript 標準的一部分,所以需要再開啟 emitDecoratorMetadata 為 true

裝飾器使用方式:@函式名稱

這個函式可以接收一個或多個參數,具體取決於是什麼類型的裝飾器。一般會有以下幾種裝飾器:

  • 類別裝飾器 ( Class Decorator )

類別裝飾器用於修改或擴充類別的定義,它接收一個 target 參數,代表被裝飾類別的建構函式。

https://ithelp.ithome.com.tw/upload/images/20230928/20141250xdmkPWrRdI.jpg

類別裝飾器在類別聲明之前聲明。

看以下範例:

const classDecorator: ClassDecorator = (target: any): void => {
  target.prototype.greet = () => {
    console.log("哈囉,威爾豬!");
  };
};

@classDecorator
class MyClass {}

const myClass = new MyClass() as any;
myClass.greet(); // 輸出: 哈囉,威爾豬!

在這個範例中,classDecorator 是一個簡單的類別裝飾器,我們在類別的建構函式原型鏈上加上 greet 方法,它被應用在 MyClass 類別上。所以 myClass 實體可以使用 greet 方法。

  • 方法裝飾器 ( Method Decorator )

方法裝飾器用於修改或擴充類別的方法,它 接收三個參數,分別是 target、propertyKey 和 descriptor。target 代表被裝飾類別的建構函式,propertyKey 代表被裝飾的方法名稱,descriptor 則是儲存屬性的選項。

https://ithelp.ithome.com.tw/upload/images/20230928/20141250VAcuJcx9lw.jpg

方法裝飾器在方法聲明之前聲明。

看以下範例:

const methodDecorator: MethodDecorator = (
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
): void => {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: number[]) {
    const result = originalMethod.apply(this, args);
    console.log(result);
  };
};

class Calculator {
  @methodDecorator
  increase(a: number, b: number): number {
    return a + b;
  }

  @methodDecorator
  decrease(a: number, b: number): number {
    return a - b;
  }
}

const calculator = new Calculator();

calculator.increase(2, 3); // 輸出: 5
calculator.decrease(2, 3); // 輸出: -1

在這個範例中,methodDecorator 方法裝飾器被應用在 Calculator 類別的 increase 和 decrease 方法上。當呼叫 increase 和 decrease 方法時,方法裝飾器中的程式碼將在方法執行前被呼叫。

這邊要注意的是,若要在 descriptor 參數的 value 欄位中呼叫原本的方法,最好用 call 或 apply 函式,並代入 this 作為參數來呼叫,這樣才可以確保 this 是該物件本身。

  • 屬性裝飾器 ( Property Decorator )

屬性裝飾器用於修改或擴充類別的屬性,它 接收兩個參數,分別是 target 和 propertyKey。target 代表被裝飾類別的建構函式,propertyKey 代表被裝飾的屬性名稱。

https://ithelp.ithome.com.tw/upload/images/20230928/20141250VU3JnvGfd8.jpg

屬性裝飾器在屬性聲明之前聲明。

看以下範例:

const propertyDecorator: PropertyDecorator = (
  target: any,
  propertyKey: string
): void => {
  let value: string = target[propertyKey];

  const getter = () => value;
  const setter = (newValue: string) => {
    value = `哈囉,${newValue}!`;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
  });
};

class Person {
  @propertyDecorator
  readonly name: string = "威爾豬";
}

const person = new Person();
console.log(person.name); // 輸出: 哈囉,威爾豬!

在這個範例中,propertyDecorator 屬性裝飾器被應用在 Person 類別的 name 屬性上。當我們使用 name 屬性時,屬性裝飾器會將屬性值重組字串。

  • 參數裝飾器 ( Parameter Decorator )

參數裝飾器用於修改或擴充方法參數,它 接收三個參數,分別是 target、propertyKey 和 parameterIndex。target 代表被裝飾類別的建構函示,propertyKey 代表被裝飾的參數所在的方法名稱,parameterIndex 代表參數在函式中的 index 位置 (由 0 開始)。

https://ithelp.ithome.com.tw/upload/images/20230928/20141250i0ThRXbhs4.jpg

參數裝飾器在參數聲明之前聲明。

看以下範例:

const parameterDecorator: ParameterDecorator = (
  target: any,
  propertyKey: string,
  parameterIndex: number
): void => {
  console.log(
    `參數位置 ${parameterIndex} 被類別 ${target.constructor.name} 的 ${propertyKey} 方法使用`
  );
};

class Calculator {
  add(@parameterDecorator a: number, @parameterDecorator b: number): number {
    return a + b;
  }
}

const calculator = new Calculator();
console.log(calculator.add(2, 3));

// 輸出
// 參數位置 1 被類別 Calculator 的 add 方法使用
// 參數位置 0 被類別 Calculator 的 add 方法使用
// 5

在這個範例中,parameterDecorator 參數裝飾器被應用在 Calculator 類別的 add 方法的兩個參數上。當呼叫 calculator.add(2, 3) 時,參數裝飾器中的程式碼將被呼叫,用於記錄參數的使用。

那如果我們想要在裝飾器傳入參數怎麼辦?這時我們就要使用到 裝飾器工廠

裝飾器工廠 ( Decorator Factory )

裝飾器工廠其實就是在裝飾器外再包一層函式,外層的函式要回傳的則是真正的裝飾器,這時外層接收的參數就可以在內層的裝飾器中使用,這其實也是閉包的一種應用。

看以下範例:

const classDecorator = (name: string, message: string) => {
  const realDecorator: ClassDecorator = (target: any): void => {
    target.prototype.greet = () => {
      console.log(`哈囉,${name}${message}!`);
    };
  };

  return realDecorator;
};

@classDecorator("威爾豬", "你好")
class MyClass {}

const myClass = new MyClass() as any;
myClass.greet(); // 輸出: 哈囉,威爾豬你好!

在這個範例中,classDecorator 傳入一個 name 參數,這時我們就可以在真正的類別裝飾器 realDecorator 上進行 name 參數的使用。

裝飾器的執行順序

裝飾器的執行是 由內往外,而在同一個類別、方法或屬性上使用的裝飾器是 由下往上,而方法內使用參數裝飾器是 從後往前,而且會 先執行參數裝飾器再執行方法裝飾器

看以下範例:

const classDecorator1: ClassDecorator = (): void => {
  console.log("我是類別裝飾器 1");
};
const classDecorator2: ClassDecorator = (): void => {
  console.log("我是類別裝飾器 2");
};
const methodDecorator1: MethodDecorator = (): void => {
  console.log("我是方法裝飾器 1");
};
const methodDecorator2: MethodDecorator = (): void => {
  console.log("我是方法裝飾器 2");
};
const propertyDecorator1: PropertyDecorator = (): void => {
  console.log("我是屬性裝飾器 1");
};
const propertyDecorator2: PropertyDecorator = (): void => {
  console.log("我是屬性裝飾器 2");
};
const parameterDecorator1: ParameterDecorator = (): void => {
  console.log("我是參數裝飾器 1");
};
const parameterDecorator2: ParameterDecorator = (): void => {
  console.log("我是參數裝飾器 2");
};

@classDecorator1 // 執行順序 8
@classDecorator2 // 執行順序 7
class MyClass1 {
  @propertyDecorator1 // 執行順序 2
  @propertyDecorator2 // 執行順序 1
  name: string;

  constructor() {
    console.log("我是建構函式 1"); // 執行順序 13
  }

  @methodDecorator1 // 執行順序 6
  @methodDecorator2 // 執行順序 5
  add(
    @parameterDecorator1 // 執行順序 4
    a: number,
    @parameterDecorator2 // 執行順序 3
    b: number
  ): void {
    console.log(a + b); // 執行順序 15
  }
}

@classDecorator1 // 執行順序 12
@classDecorator2 // 執行順序 11
class MyClass2 {
  @propertyDecorator1 // 執行順序 10
  @propertyDecorator2 // 執行順序 9
  age: string;

  constructor() {
    console.log("我是建構函式 2"); // 執行順序 14
  }

  multiple(a: number, b: number): void {
    console.log(a * b); // 執行順序 16
  }
}

const myClass1 = new MyClass1();
const myClass2 = new MyClass2();

myClass1.add(2, 3); // 輸出: 5
myClass2.multiple(4, 5); // 輸出: 20

// 輸出:
// 我是屬性裝飾器 2
// 我是屬性裝飾器 1
// 我是參數裝飾器 2
// 我是參數裝飾器 1
// 我是方法裝飾器 2
// 我是方法裝飾器 1
// 我是類別裝飾器 2
// 我是類別裝飾器 1
// 我是屬性裝飾器 2
// 我是屬性裝飾器 1
// 我是類別裝飾器 2
// 我是類別裝飾器 1
// 我是建構函式 1
// 我是建構函式 2
// 5
// 20

每種類型的裝飾器都有其特定的應用場景,並可以用於實現不同的功能。它們可以應用在類別、方法、屬性和參數上,並在開發框架、函式庫等有廣泛的應用,例如路由、驗證、log 記錄等。在使用裝飾器時應考慮程式碼的結構和可讀性,並謹慎選擇適當的裝飾器來實現需求,避免濫用,過多的裝飾器可能會使程式碼變得複雜和難以維護。


上一篇
Utility 型別 Ⅱ
下一篇
聲明文件 ( Declaration Files )
系列文
用不到 30 天學會基本 TypeScript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言