1. 程式人生 > >聊聊Typescript中的設計模式——裝飾器篇(decorators)

聊聊Typescript中的設計模式——裝飾器篇(decorators)


  隨著Typescript的普及,在KOA2和nestjs等nodejs框架中經常看到類似於java spring中註解的寫法。本文從裝飾模式出發,聊聊Typescipt中的裝飾器和註解。

  • 什麼是裝飾者模式
  • Typescript中的裝飾器
  • Typescript中的註解
  • 總結

原文地址在:github.com/fortheallli…

歡迎star

一、什麼是裝飾者模式

  最近在看nestjs等支援Typescript的node框架,經常看到這樣一種寫法:

import { Controller, Get } from '@nestjs/common';

@Controller('cats')

export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}
複製程式碼

  上述程式碼定義了一個處理url為“/cats”的控制器,該控制器對於url為“/cats”的get方法執行findAll()函式,返回相應的字串。

  在上述的程式碼中,用@Controller('cats')修飾CatsController類,通過@Get來修飾類中的findAll方法,這就是典型的裝飾者模式。通過@Controller('cats')和@Get修飾後的類CatsController,簡單來說,就是擁有了豐富的“內涵”。

下面看看具體裝飾者模式的定義:

我們知道繼承模式是豐富子元素“內涵”的一種重要方式,不管是繼承介面還是子類繼承基類。而裝飾者模式可以在不改變繼承關係的前提下,包裝先有的模組,使其內涵更加豐富,並不會影響到原來的功能。與繼承相比,更加的靈活。

javascript中的裝飾器處於建議徵集的第二階段,通過babel和Typescrit都可以實現裝飾器的語法。

二、Typescript中的裝飾器

Typescript中的裝飾器與類相關,分別可以修飾類的例項函式和靜態函式、類本身、類的屬性、類中函式的引數以及類的set/get存取器,下面來意義介紹。

(1)、類方法的裝飾器

下面來介紹一下用裝飾器來修飾函式,首先來看一個例子:

let temple;
function log(target, key, descriptor) {
  console.log(`${key} was called!`);
  temple = target;
}
class P {
    @log
    foo() {
      console.log('Do something');
    }
}

const p = new P()
p.foo()
console.log(P.prototype === temple) //true
複製程式碼

上述是例項方法foo中我們用log函式修飾,log函式接受三個引數,通過P.prototype === temple(target)可以判斷,在類的例項函式的裝飾器函式第一個引數為類的原型,第二個引數為函式名本身,第三個引數為該函式的描述屬性。

具體總結如下,對於類的函式的裝飾器函式,依次接受的引數為:

  • target:如果修飾的是類的例項函式,那麼target就是類的原型。如果修飾的是類的靜態函式,那麼target就是類本身。
  • key: 該函式的函式名。
  • descriptor:該函式的描述屬性,比如 configurable、value、enumerable等。

從上述的例子中我們可以看到,用裝飾器來修飾相應的類的函式十分方便:

@log
foo() {
  ...
}
複製程式碼

(2)、類的裝飾器

裝飾函式也可以直接修飾類:

let temple
function foo(target){
   console.log(target);
   temple = target
}
@foo
class P{
   constructor(){
     
   }
}

const p = new P();
temple === P //true
複製程式碼

當裝飾函式直接修飾類的時候,裝飾函式接受唯一的引數,這個引數就是該被修飾類本身。上述的例子中,輸出的target就是類P的本身。

此外,在修飾類的時候,如果裝飾函式有返回值,該返回值會重新定義這個類,也就是說當裝飾函式有返回值時,其實是生成了一個新類,該新類通過返回值來定義。

舉例來說:

function foo(target){
   return class extends target{
      name = 'Jony';
      sayHello(){
         console.log("Hello "+ this.name)
      }
   }
}
@foo
class P{
   constructor(){
     
   }
}

const p = new P();
p.sayHello(); // 會輸出Hello Jony
複製程式碼

上面的例子可以看到,當裝飾函式foo有返回值時,實際上P類已經被返回值所代表的新類所代替,因此P的例項p擁有sayHello方法。

(3)、類的屬性的裝飾器

下面我們來看類的屬性的裝飾器,裝飾函式修飾類的屬性時,在類例項化的時候呼叫屬性的裝飾函式,舉例來說:

function foo(target,name){
   console.log("target is",target);
   console.log("name is",name)
}
class P{
   @foo
   name = 'Jony'
}
const p = new P();
//會依次輸出 target is f P()  name is Jony
複製程式碼

這裡對於類的屬性的裝飾器函式接受兩個引數,對於靜態屬性而言,第一個引數是類本身,對於例項屬性而言,第一個引數是類的原型,第二個引數是指屬性的名字。

(4)、類函式引數的裝飾器

接著來看類函式引數的裝飾器,類函式的引數裝飾器可以修飾類的構建函式中的引數,以及類中其他普通函式中的引數。該裝飾器在類的方法被呼叫的時候執行,下面來看例項:

function foo(target,key,index){
   console.log("target is",target);
   console.log("key is",key);
   console.log("index is",index)
}
class P{
   test(@foo a){
   }
}
const p = new P();
p.test("Hello Jony")
// 依次輸出 f P() , test , 0 
複製程式碼

類函式引數的裝飾器函式接受三個引數,依次為類本身,類中該被修飾的函式本身,以及被修飾的引數在引數列表中的索引值。上述的例子中,會依次輸出 f P() 、test和0。再次明確一下修飾函式引數的裝飾器函式中的引數含義:

  • target: 類本身
  • key:該引數所在的函式的函式名
  • index: 該引數在函式引數列表中的索引值

從上面的Typescrit中在基類中常用的裝飾器後,我們發現:

裝飾器可以起到分離複雜邏輯的功能,且使用上極其簡單方便。與繼承相比,也更加靈活,可以從裝飾類,到裝飾類函式的引數,可以說武裝到了“牙齒”。

三、Typescript中的註解

在瞭解了Typescrit中的裝飾器之後,接著我們來看Typescrit中的註解。

什麼是註解,所謂註解的定義就是:

為相應的類附加元資料支援

所謂元資料可以簡單的解釋,就是修飾資料的資料,比如一個人有name,age等資料屬性,那麼name和age這些欄位就是為了修飾資料的資料,可以簡單的稱為元資料。

元資料簡單來說就是可以修飾某些資料的欄位。下面給出裝飾器和註解的解釋和區別:

  • 裝飾器:定義劫持,可以對類,類的方法,類的屬性以及類的方法的入參進行修改。不提供元資料的支援。
  • 註解:僅提供元資料的支援。

兩者之間的聯絡:

通過註解新增元資料,然後在裝飾器中獲取這些元資料,完成對類、類的方法等等的修改,可以在裝飾器中新增元資料的支援,比如可以可以在裝飾器工廠函式以及裝飾器函式中新增元資料支援等

(1)、Typescript中的元資料操作

可以通過reflect-metadata包來實現對於元資料的操作。首先我們來看reflect-metadata的使用,首先定義使用元資料的函式:

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
複製程式碼

這裡的format可以作為裝飾器函式的工廠函式,因為format函式返回的是一個裝飾器函式,上述的方法定義了元資料Sysmbol("format"),用Sysmbol的原因是為了防止元資料中的欄位重複,而format定義了取元資料中相應欄位的功能。

接著我們來在類中使用相應的元資料:

class Greeter {
    @format("Hello, %s")
    name: string;

    constructor(name: string) {
        this.name = message;
    }
    sayHello() {
        let formatString = getFormat(this, "name");
        return formatString.replace("%s", this.name);
    }
}

const g = new Greeter("Jony");
console.log(g.sayHello());
複製程式碼

在上述中,我們在name屬性的裝飾器工廠函式,執行@format("Hello, %s"),返回一個裝飾器函式,且該裝飾器函式修飾了Greeter類的name屬性,將“name”屬性的值寫入為"Hello, %s"。

然後再sayHello方法中,通過getFormat(this,"name")取到formatString為“Hello,%s”.

四、總結

通過裝飾器,可以方便的修飾類,以及類的方法,類的屬性等,相比於繼承而言更加靈活,此外,通過註解的方法,可以在Typescript中引入元資料,實現超程式設計等。特別是在angularjs、nestjs中,大量使用了註解,特別是nestjs構建了類似於java springMVC式的web框架。