Javascript 裝飾器極速指南


本文主要參考自 https://cabbageapps.com/fell-love-js-decorators/。 并未按原文嚴(yán)格翻譯,做了重新整理和編輯,部分內(nèi)容做了大范圍修改。

pablo.png

Decorators 是ES7中添加的JavaScript新特性。熟悉Typescript的同學(xué)應(yīng)該更早的接觸到這個(gè)特性,TypeScript早些時(shí)候已經(jīng)支持Decorators的使用,而且提供了ES5的支持。本文會(huì)對(duì)Decorators做詳細(xì)的講解,相信你會(huì)體驗(yàn)到它給編程帶來(lái)便利和優(yōu)雅。

我在專(zhuān)職做前端開(kāi)發(fā)之前, 是一名專(zhuān)業(yè)的.NET程序員,對(duì).NET中的“特性”使用非常熟悉。在類(lèi)、方法或者屬性上寫(xiě)上一個(gè)中括號(hào),中括號(hào)里面初始化一個(gè)特性,就會(huì)對(duì)類(lèi),方法或者屬性的行為產(chǎn)生影響。這在AOP編程,以及ORM框架中特別有用,就像魔法一樣。 但是當(dāng)時(shí)JavaScript并沒(méi)有這樣的特性。在TypeScript中第一次使用Decorators,是因?yàn)槲覀円獙?duì)整個(gè)應(yīng)用程序的上下文信息做序列化處理,需要一種簡(jiǎn)單的方法,在原來(lái)的領(lǐng)域模型上打上一個(gè)標(biāo)簽來(lái)標(biāo)識(shí)是否會(huì)序列化或者序列化的行為控制,這種場(chǎng)景下Decorators發(fā)揮了它的威力。 后來(lái)我們需要重構(gòu)我們的狀態(tài)管理,在可變的類(lèi)定義和不可變對(duì)象的應(yīng)用間進(jìn)行轉(zhuǎn)換,如果使用Decorators,不論從編的便利性還是解耦的角度都產(chǎn)生了令人驚喜的效果。 一直想把Decorators的相關(guān)使用整理出一個(gè)通俗的文檔,使用最簡(jiǎn)單的方式來(lái)闡述這一話題,一直沒(méi)有下筆。無(wú)意間在網(wǎng)絡(luò)上發(fā)現(xiàn)了一篇文章(https://cabbageapps.com/fell-love-js-decorators/) , 這篇文章的行文和我要表達(dá)的內(nèi)容正好相符,于是拿過(guò)來(lái)做重新編輯和改編。喜歡看英文的同學(xué)可以點(diǎn)擊鏈接閱讀原文。

giphy.gif

1.0 裝飾器模式

如果我們?cè)谒阉饕嬷兄苯铀阉鳌癲ecorators”或者“裝飾器”,和編程相關(guān)的結(jié)果中,會(huì)看到設(shè)計(jì)模式中的裝飾器模式的介紹。

3.png

更直觀的例子如下:

4.png

上圖中WeaponAccessory就是一個(gè)裝飾器,他們添加額外的方法和熟悉到基類(lèi)上。如果你看不明白沒(méi)關(guān)系,跟隨我一步步地實(shí)現(xiàn)你自己的裝飾器,自然就會(huì)明白了。下面這張圖,可以幫你直觀的理解裝飾器。

5.gif

我們簡(jiǎn)單的理解裝飾器,可以認(rèn)為它是一種包裝,對(duì)對(duì)象,方法,熟悉的包裝。當(dāng)我們需要訪問(wèn)一個(gè)對(duì)象的時(shí)候,如果我們通過(guò)這個(gè)對(duì)象外圍的包裝去訪問(wèn)的話,被這個(gè)包裝附加的行為就會(huì)被觸發(fā)。例如 一把加了消聲器的槍。消聲器就是一個(gè)裝飾,但是它和原來(lái)的槍成為一個(gè)整體,開(kāi)槍的時(shí)候消聲器就會(huì)發(fā)生作用。

從面向?qū)ο蟮慕嵌群芎美斫膺@個(gè)概念。那么我們?nèi)绾卧贘avaScript中使用裝飾器呢?

1.1 開(kāi)始 Decorators 之旅

Decorators 是ES7才支持的新特性,但是借助Babel 和 TypesScript,我們現(xiàn)在就可以使用它了, 本文以TypesScript為例。

首先修改tsconfig.json文件,設(shè)置 experimentalDecorators 和 emitDecoratorMetadata為true。

{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules",
  ]
}
6.png

我們先從效果入手,然后再層層剖析。先看下面的一段代碼:

function leDecorator(target, propertyKey: string, descriptor: PropertyDescriptor): any {
    var oldValue = descriptor.value;

    descriptor.value = function() {
      console.log(`Calling "${propertyKey}" with`, arguments,target);
      let value = oldValue.apply(null, [arguments[1], arguments[0]]);

      console.log(`Function is executed`);
      return value + "; This is awesome";
    };

    return descriptor;
  }

  class JSMeetup {
    speaker = "Ruban";
    //@leDecorator
    welcome(arg1, arg2) {
      console.log(`Arguments Received are ${arg1} ${arg2}`);
      return `${arg1} ${arg2}`;
    }
  }

  const meetup = new JSMeetup();

  console.log(meetup.welcome("World", "Hello"));
7.png

運(yùn)行上面的代碼,得到的結(jié)果如下:

8.png

下面我們修改代碼,將第17行的注釋放開(kāi)。

9.png

再次運(yùn)行代碼,結(jié)果如下:

10.png

注意上圖中左側(cè)的輸出結(jié)果,和右側(cè)顯示的代碼行號(hào)。我們現(xiàn)在可以肯定的是,加上了 @leDecorator 標(biāo)簽之后,函數(shù)welcome的行為發(fā)生了改變,觸發(fā)改變的地方是leDecorator函數(shù)。 根據(jù)我們上面對(duì)裝飾器的基本理解,我們可以認(rèn)為leDecorator是welcome的裝飾器。
<b>裝飾器和被裝飾者之間通過(guò) @ 符進(jìn)行連接</b>。

在JavaScript層面我們已經(jīng)感性的認(rèn)識(shí)了裝飾器,我們的代碼裝飾的是一個(gè)函數(shù)。在JavaScript中,一共有4類(lèi)裝飾器:

  • Method Decorator 函數(shù)裝飾器
  • Property Decorators 熟悉裝飾器
  • Class Decorator 類(lèi)裝飾器
  • Parameter Decorator 參數(shù)裝飾器

下面我們逐一進(jìn)行攻破!Come on!

11.jpg

1.2 函數(shù)裝飾器

第一個(gè)要被攻破的裝飾器是函數(shù)裝飾器,這一節(jié)是本文的核心內(nèi)容,我們將通過(guò)對(duì)函數(shù)裝飾器的講解來(lái)洞察JavaScript Decorators的本質(zhì)。

通過(guò)使用 函數(shù)裝飾器,我們可以控制函數(shù)的輸入和輸出。

下面是函數(shù)裝飾器的定義:

MethodDecorator = <T>(target: Object, key: string, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | Void;

只要遵循上面的定義,我們就可以自定義一個(gè)函數(shù)裝飾器,三個(gè)參數(shù)的含義如下:

  • target -> 被裝飾的對(duì)象
  • key -> 被裝飾的函數(shù)名
  • descriptor -> 被傳遞過(guò)來(lái)的屬性的屬性描述符. 可以通過(guò) Object.getOwnPropertyDescriptor()方法來(lái)查看屬性描述符。

關(guān)于屬性描述符更詳細(xì)內(nèi)容 可以參考 http://www.itdecent.cn/p/19529527df80

簡(jiǎn)單來(lái)講,屬性描述符可以用來(lái)配置一個(gè)對(duì)象的某個(gè)屬性的返回值,get/set 行為,是否可以被刪除,是否可以被修改,是否可以被枚舉等特性。為了你能順暢的理解裝飾器,我們下面看一個(gè)直觀一點(diǎn)的例子。

打開(kāi)瀏覽器控制臺(tái),輸入如下代碼:

var o, d;
var o = { get foo() { return 17; }, bar:17, foobar:function(){return "FooBar"} };

d = Object.getOwnPropertyDescriptor(o, 'foo');
console.log(d);
d = Object.getOwnPropertyDescriptor(o, 'bar');
console.log(d);
d = Object.getOwnPropertyDescriptor(o, 'foobar');
console.log(d);

結(jié)果如下:

12.png

這里我們定義了一個(gè)對(duì)象o,定義了三個(gè)屬性——foo,bar和foobar,之后通過(guò)Object.getOwnPropertyDescriptor()獲取每個(gè)屬性的描述符并打印出來(lái)。下面我們對(duì)value , enumerable , configurable 和 writable 做簡(jiǎn)要的說(shuō)明。

  • value – >字面值或者函數(shù)/屬性計(jì)算后的返回值。
  • enumerable -> 是否可以被枚舉 (是否可以在 (for x in obj)循環(huán)中被枚舉出來(lái))
  • configurable – >屬性是否可以被配置
  • writable -> 屬性是否是可寫(xiě)的.

每個(gè)屬性或者方法都有自己的一個(gè)描述符,通過(guò)描述符我們可以修改屬性的行為或者返回值。下面關(guān)鍵來(lái)了:

<b>裝飾器的本質(zhì)就是修改描述符</b>

是時(shí)候動(dòng)手寫(xiě)一個(gè)裝飾器了。

1.2.1 方法裝飾器實(shí)例

下面我們通過(guò)方法裝飾器來(lái)修改一個(gè)函數(shù)的輸入和輸出。

function leDecorator(target, propertyKey: string, descriptor: PropertyDescriptor): any {
    var oldValue = descriptor.value;

    descriptor.value = function() {
      console.log(`Calling "${propertyKey}" with`, arguments,target);
      // Executing the original function interchanging the arguments
      let value = oldValue.apply(null, [arguments[1], arguments[0]]);
      //returning a modified value
      return value + "; This is awesome";
    };

    return descriptor;
  }

  class JSMeetup {
    speaker = "Ruban";
    //@leDecorator
    welcome(arg1, arg2) {
      console.log(`Arguments Received are ${arg1}, ${arg2}`);
      return `${arg1} ${arg2}`;
    }
  }

  const meetup = new JSMeetup();

  console.log(meetup.welcome("World", "Hello"));

在不使用裝飾器的時(shí)候,輸出值為:

Arguments Received are World, Hello
World Hello

啟用裝飾器后,輸出值為:

Calling "welcome" with { '0': 'World', '1': 'Hello' } JSMeetup {}
Arguments Received are Hello, World
Hello World; This is awesome

我們看到,方法輸出值發(fā)成了變化。現(xiàn)在去看我們定義的方法裝飾器,通過(guò)參數(shù),leDecorator在執(zhí)行時(shí)獲取了調(diào)用對(duì)象的名稱(chēng),被裝飾方法的參數(shù),被裝飾方法的描述符。 首先通過(guò)oldValue變量保存了方法描述符的原值,即我們定義的welcome方法。接下來(lái)對(duì)descriptor.value進(jìn)行了重新賦值。

13.png

在新的函數(shù)中首先調(diào)用了原函數(shù),獲得了返回值,然后修改了返回值。 最后return descriptor,新的descriptor會(huì)被應(yīng)用到welcome方法上,此時(shí)整合函數(shù)體已經(jīng)被替換了。

通過(guò)使用裝飾器,我們實(shí)現(xiàn)了對(duì)原函數(shù)的包裝,可以修改方法的輸入和輸出,這意味著我們可以應(yīng)用各種想要的魔法效果到目標(biāo)方法上。

14.gif

這里有幾點(diǎn)需要注意的地方:

  • 裝飾器在class被聲明的時(shí)候被執(zhí)行,而不是class實(shí)例化的時(shí)候。
  • 方法裝飾器返回一個(gè)值
  • 存儲(chǔ)原有的描述符并且返回一個(gè)新的描述符是我們推薦的做法. 這在多描述符應(yīng)用的場(chǎng)景下非常有用。
  • 設(shè)置描述符的value的時(shí)候,不要使用箭頭函數(shù)。

現(xiàn)在我們完成并理解了第一個(gè)方法裝飾器。下面我們來(lái)學(xué)校屬性裝飾器。

1.3 屬性裝飾器

屬性裝飾器和方法裝飾器很類(lèi)似,通過(guò)屬性裝飾器,我們可以用來(lái)重新定義getters、setters,修改enumerable, configurable等屬性。

屬性裝飾器定義如下:

PropertyDecorator = (target: Object, key: string) => void;

參數(shù)說(shuō)明如下:

  • target:屬性擁有者
  • key:屬性名

在具體使用屬性裝飾器之前,我們先來(lái)簡(jiǎn)單了解下Object.defineProperty方法。Object.defineProperty方法通常用來(lái)動(dòng)態(tài)給一個(gè)對(duì)象添加或者修改屬性。下面是一段示例:

var o = { get foo() { return 17; }, bar:17, foobar:function(){return "FooBar"} };

Object.defineProperty(o, 'myProperty', {
get: function () {
return this['myProperty'];
},
set: function (val) {
this['myProperty'] = val;
},
enumerable:true,
configurable:true
});
16.png

在調(diào)試控制臺(tái)測(cè)試上面的代碼。

15.png

從結(jié)果中,我們看到,利用Object.defineProperty,我們動(dòng)態(tài)添給對(duì)象添加了屬性。下面我們基于Object.defineProperty來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的屬性裝飾器。

function realName(target, key: string): any {
    // property value
    var _val = target[key];

    // property getter
    var getter = function () {
      return "Ragularuban(" + _val + ")";
    };

    // property setter
    var setter = function (newVal) {
      _val = newVal;
    };

    // Create new property with getter and setter
    Object.defineProperty(target, key, {
      get: getter,
      set: setter
    });
  }

  class JSMeetup {
    //@realName
    public myName = "Ruban";
    constructor() {
    }
    greet() {
      return "Hi, I'm " + this.myName;
    }
  }

  const meetup = new JSMeetup();
  console.log(meetup.greet());
  meetup.myName = "Ragul";
  console.log(meetup.greet());
17.png

在不適用裝飾器時(shí),輸出結(jié)果為:

Hi, I'm Ruban
Hi, I'm Ragul

啟用裝飾器之后,結(jié)果為:

Hi, I'm Ragularuban(Ruban)
Hi, I'm Ragularuban(Ragul)

是不是很簡(jiǎn)單呢? 接下來(lái)是Class裝飾器。

1.4 Class 裝飾器

Class裝飾器是通過(guò)操作Class的構(gòu)造函數(shù),來(lái)實(shí)現(xiàn)對(duì)Class的相關(guān)屬性和方法的動(dòng)態(tài)添加和修改。
下面是Class裝飾器的定義:

ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction;

ClassDecorator只接收一個(gè)參數(shù),就是Class的構(gòu)造函數(shù)。下面的示例代碼,修改了類(lèi)原有的屬性speaker,并動(dòng)態(tài)添加了一個(gè)屬性extra。

function AwesomeMeetup<T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor implements extra {
      speaker: string = "Ragularuban";
      extra = "Tadah!";
    }
  }

  //@AwesomeMeetup
  class JSMeetup {
    public speaker = "Ruban";
    constructor() {
    }
    greet() {
      return "Hi, I'm " + this.speaker;
    }
  }

  interface extra {
    extra: string;
  }

  const meetup = new JSMeetup() as JSMeetup & extra;
  console.log(meetup.greet());
  console.log(meetup.extra);

在不啟用裝飾器的情況下輸出值為:

18.png

在啟用裝飾器的情況下,輸出結(jié)果為:

19.png

這里需要注意的是,<b>構(gòu)造函數(shù)只會(huì)被調(diào)用一次</b>。

下面我來(lái)學(xué)習(xí)最后一種裝飾器,參數(shù)裝飾器。

1.5 參數(shù)裝飾器

如果通過(guò)上面講過(guò)的裝飾器來(lái)推論參數(shù)裝飾器的作用,可能會(huì)是修改參數(shù),但事實(shí)上并非如此。參數(shù)裝飾器往往用來(lái)對(duì)特殊的參數(shù)進(jìn)行標(biāo)記,然后在方法裝飾器中讀取對(duì)應(yīng)的標(biāo)記,執(zhí)行進(jìn)一步的操作。例如:

function logParameter(target: any, key: string, index: number) {
    var metadataKey = `myMetaData`;
    if (Array.isArray(target[metadataKey])) {
      target[metadataKey].push(index);
    }
    else {
      target[metadataKey] = [index];
    }
  }

  function logMethod(target, key: string, descriptor: any): any {
    var originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {

      var metadataKey = `myMetaData`;
      var indices = target[metadataKey];
      console.log('indices', indices);
      for (var i = 0; i < args.length; i++) {

        if (indices.indexOf(i) !== -1) {
          console.log("Found a marked parameter at index" + i);
          args[i] = "Abrakadabra";
        }
      }
      var result = originalMethod.apply(this, args);
      return result;

    }
    return descriptor;
  }

  class JSMeetup {
    //@logMethod
    public saySomething(something: string, @logParameter somethingElse: string): string {
      return something + " : " + somethingElse;
    }
  }

  let meetup = new JSMeetup();

  console.log(meetup.saySomething("something", "Something Else"));

20.png

上面的代碼中,我們定義了一個(gè)參數(shù)裝飾器,該裝飾器將被裝飾的參數(shù)放到一個(gè)指定的數(shù)組中。在方法裝飾器中,查找被標(biāo)記的參數(shù),做進(jìn)一步的處理
不啟用裝飾器的情況下,輸出結(jié)果如下:

21.png

啟用裝飾器的情況下,輸出結(jié)果如下:

22.png

1.6 小結(jié)

現(xiàn)在我們已經(jīng)學(xué)習(xí)了所有裝飾器的使用,下面總結(jié)一下關(guān)鍵用法:

  • 方法裝飾器的核心是 方法描述符
  • 屬性裝飾器的核心是 Object.defineProperty
  • Class裝飾器的核心是 構(gòu)造函數(shù)
  • 參數(shù)裝飾器的主要作用是標(biāo)記,要結(jié)合方法裝飾器來(lái)使用

下面是參考文章:
https://www.typescriptlang.org/docs/handbook/decorators.html

https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Decorators.md

https://survivejs.com/react/appendices/understanding-decorators/

https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841

https://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-ii
https://github.com/arolson101/typescript-decorators


更多精彩內(nèi)容,歡迎關(guān)注玄魂工作室微信訂閱號(hào)。


24.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容