JavaScript 裝飾器極速指南


Decorators 是ES7中添加的JavaScript新特性。熟悉Typescript的同窗應該更早的接觸到這個特性,TypeScript早些時候已經支持Decorators的使用,並且提供了ES5的支持。本文會對Decorators作詳細的講解,相信你會體驗到它給編程帶來便利和優雅。javascript

我在專職作前端開發以前, 是一名專業的.NET程序員,對.NET中的「特性」使用很是熟悉。在類、方法或者屬性上寫上一個中括號,中括號裏面初始化一個特性,就會對類,方法或者屬性的行爲產生影響。這在AOP編程,以及ORM框架中特別有用,就像魔法同樣。 可是當時JavaScript並無這樣的特性。在TypeScript中第一次使用Decorators,是由於咱們要對整個應用程序的上下文信息作序列化處理,須要一種簡單的方法,在原來的領域模型上打上一個標籤來標識是否會序列化或者序列化的行爲控制,這種場景下Decorators發揮了它的威力。 後來咱們須要重構咱們的狀態管理,在可變的類定義和不可變對象的應用間進行轉換,若是使用Decorators,不論從編的便利性仍是解耦的角度都產生了使人驚喜的效果。 一直想把Decorators的相關使用整理出一個通俗的文檔,使用最簡單的方式來闡述這一話題,一直沒有下筆。無心間在網絡上發現了一篇文章(https://cabbageapps.com/fell-love-js-decorators/) , 這篇文章的行文和我要表達的內容正好相符,因而拿過來作從新編輯和改編。喜歡看英文的同窗能夠點擊連接閱讀原文。html

1.0 裝飾器模式

若是咱們在搜索引擎中直接搜索「decorators」或者「裝飾器」,和編程相關的結果中,會看到設計模式中的裝飾器模式的介紹。前端

更直觀的例子以下:java

上圖中WeaponAccessory就是一個裝飾器,他們添加額外的方法和熟悉到基類上。若是你看不明白不要緊,跟隨我一步步地實現你本身的裝飾器,天然就會明白了。下面這張圖,能夠幫你直觀的理解裝飾器。node

5.gif

咱們簡單的理解裝飾器,能夠認爲它是一種包裝,對對象,方法,熟悉的包裝。當咱們須要訪問一個對象的時候,若是咱們經過這個對象外圍的包裝去訪問的話,被這個包裝附加的行爲就會被觸發。例如 一把加了消聲器的槍。消聲器就是一個裝飾,可是它和原來的槍成爲一個總體,開槍的時候消聲器就會發生做用。react

從面向對象的角度很好理解這個概念。那麼咱們如何在JavaScript中使用裝飾器呢?git

1.1 開始 Decorators 之旅

Decorators 是ES7才支持的新特性,可是藉助Babel 和 TypesScript,咱們如今就可使用它了, 本文以TypesScript爲例。程序員

首先修改tsconfig.json文件,設置 experimentalDecorators 和 emitDecoratorMetadata爲true。github

{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules",
  ]
}
複製代碼

咱們先從效果入手,而後再層層剖析。先看下面的一段代碼:typescript

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"));
複製代碼

運行上面的代碼,獲得的結果以下:

下面咱們修改代碼,將第17行的註釋放開。

再次運行代碼,結果以下:

注意上圖中左側的輸出結果,和右側顯示的代碼行號。咱們如今能夠確定的是,加上了 @leDecorator 標籤以後,函數welcome的行爲發生了改變,觸發改變的地方是leDecorator函數。 根據咱們上面對裝飾器的基本理解,咱們能夠認爲leDecorator是welcome的裝飾器。 裝飾器和被裝飾者之間經過 @ 符進行鏈接

在JavaScript層面咱們已經感性的認識了裝飾器,咱們的代碼裝飾的是一個函數。在JavaScript中,一共有4類裝飾器:

  • Method Decorator 函數裝飾器
  • Property Decorators 熟悉裝飾器
  • Class Decorator 類裝飾器
  • Parameter Decorator 參數裝飾器

下面咱們逐一進行攻破!Come on!

1.2 函數裝飾器

第一個要被攻破的裝飾器是函數裝飾器,這一節是本文的核心內容,咱們將經過對函數裝飾器的講解來洞察JavaScript Decorators的本質。

經過使用 函數裝飾器,咱們能夠控制函數的輸入和輸出。

下面是函數裝飾器的定義:

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

只要遵循上面的定義,咱們就能夠自定義一個函數裝飾器,三個參數的含義以下:

  • target -> 被裝飾的對象
  • key -> 被裝飾的函數名
  • descriptor -> 被傳遞過來的屬性的屬性描述符. 能夠經過 Object.getOwnPropertyDescriptor()方法來查看屬性描述符。

關於屬性描述符更詳細內容 能夠參考 https://www.jianshu.com/p/19529527df80 。

簡單來說,屬性描述符能夠用來配置一個對象的某個屬性的返回值,get/set 行爲,是否能夠被刪除,是否能夠被修改,是否能夠被枚舉等特性。爲了你能順暢的理解裝飾器,咱們下面看一個直觀一點的例子。

打開瀏覽器控制檯,輸入以下代碼:

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);
複製代碼

結果以下:

這裏咱們定義了一個對象o,定義了三個屬性——foo,bar和foobar,以後經過Object.getOwnPropertyDescriptor()獲取每一個屬性的描述符並打印出來。下面咱們對value , enumerable , configurable 和 writable 作簡要的說明。

  • value – >字面值或者函數/屬性計算後的返回值。
  • enumerable -> 是否能夠被枚舉 (是否能夠在 (for x in obj)循環中被枚舉出來)
  • configurable – >屬性是否能夠被配置
  • writable -> 屬性是不是可寫的.

每一個屬性或者方法都有本身的一個描述符,經過描述符咱們能夠修改屬性的行爲或者返回值。下面關鍵來了:

裝飾器的本質就是修改描述符

是時候動手寫一個裝飾器了。

1.2.1 方法裝飾器實例

下面咱們經過方法裝飾器來修改一個函數的輸入和輸出。

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"));
複製代碼

在不使用裝飾器的時候,輸出值爲:

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
複製代碼

咱們看到,方法輸出值發成了變化。如今去看咱們定義的方法裝飾器,經過參數,leDecorator在執行時獲取了調用對象的名稱,被裝飾方法的參數,被裝飾方法的描述符。 首先經過oldValue變量保存了方法描述符的原值,即咱們定義的welcome方法。接下來對descriptor.value進行了從新賦值。

在新的函數中首先調用了原函數,得到了返回值,而後修改了返回值。 最後return descriptor,新的descriptor會被應用到welcome方法上,此時整合函數體已經被替換了。

經過使用裝飾器,咱們實現了對原函數的包裝,能夠修改方法的輸入和輸出,這意味着咱們能夠應用各類想要的魔法效果到目標方法上。

這裏有幾點須要注意的地方:

  • 裝飾器在class被聲明的時候被執行,而不是class實例化的時候。
  • 方法裝飾器返回一個值
  • 存儲原有的描述符而且返回一個新的描述符是咱們推薦的作法. 這在多描述符應用的場景下很是有用。
  • 設置描述符的value的時候,不要使用箭頭函數。

如今咱們完成並理解了第一個方法裝飾器。下面咱們來學校屬性裝飾器。

1.3 屬性裝飾器

屬性裝飾器和方法裝飾器很相似,經過屬性裝飾器,咱們能夠用來從新定義getters、setters,修改enumerable, configurable等屬性。

屬性裝飾器定義以下:

PropertyDecorator = (target: Object, key: string) => void;
複製代碼

參數說明以下:

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

在具體使用屬性裝飾器以前,咱們先來簡單瞭解下Object.defineProperty方法。Object.defineProperty方法一般用來動態給一個對象添加或者修改屬性。下面是一段示例:

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
});
複製代碼

在調試控制檯測試上面的代碼。

從結果中,咱們看到,利用Object.defineProperty,咱們動態添給對象添加了屬性。下面咱們基於Object.defineProperty來實現一個簡單的屬性裝飾器。

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());
複製代碼

在不適用裝飾器時,輸出結果爲:

Hi, I'm Ruban Hi, I'm Ragul
複製代碼

啓用裝飾器以後,結果爲:

Hi, I'm Ragularuban(Ruban) Hi, I'm Ragularuban(Ragul)
複製代碼

是否是很簡單呢? 接下來是Class裝飾器。

1.4 Class 裝飾器

Class裝飾器是經過操做Class的構造函數,來實現對Class的相關屬性和方法的動態添加和修改。 下面是Class裝飾器的定義:

ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction; 複製代碼

ClassDecorator只接收一個參數,就是Class的構造函數。下面的示例代碼,修改了類原有的屬性speaker,並動態添加了一個屬性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);
複製代碼

在不啓用裝飾器的狀況下輸出值爲:

在啓用裝飾器的狀況下,輸出結果爲:

這裏須要注意的是,構造函數只會被調用一次

下面我來學習最後一種裝飾器,參數裝飾器。

1.5 參數裝飾器

若是經過上面講過的裝飾器來推論參數裝飾器的做用,可能會是修改參數,但事實上並不是如此。參數裝飾器每每用來對特殊的參數進行標記,而後在方法裝飾器中讀取對應的標記,執行進一步的操做。例如:

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"));

複製代碼

上面的代碼中,咱們定義了一個參數裝飾器,該裝飾器將被裝飾的參數放到一個指定的數組中。在方法裝飾器中,查找被標記的參數,作進一步的處理 不啓用裝飾器的狀況下,輸出結果以下:

啓用裝飾器的狀況下,輸出結果以下:

1.6 小結

如今咱們已經學習了全部裝飾器的使用,下面總結一下關鍵用法:

  • 方法裝飾器的核心是 方法描述符
  • 屬性裝飾器的核心是 Object.defineProperty
  • Class裝飾器的核心是 構造函數
  • 參數裝飾器的主要做用是標記,要結合方法裝飾器來使用

更多前端好文,關注微信訂閱號「玄魂工做室」,回覆「qd」 便可

玄魂工做室
下面是參考文章: 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

相關文章
相關標籤/搜索