從ES6從新認識JavaScript設計模式: 裝飾器模式

1 什麼是裝飾器模式

Decorator

向一個現有的對象添加新的功能,同時又不改變其結構的設計模式被稱爲裝飾器模式(Decorator Pattern),它是做爲現有的類的一個包裝(Wrapper)。

能夠將裝飾器理解爲遊戲人物購買的裝備,例如LOL中的英雄剛開始遊戲時只有基礎的攻擊力和法強。可是在購買的裝備後,在觸發攻擊和技能時,可以享受到裝備帶來的輸出加成。咱們能夠理解爲購買的裝備給英雄的攻擊和技能的相關方法進行了裝飾。前端

這裏推薦一篇淘寶前端團隊的博文,頗有趣的以鋼鐵俠的例子來說解了裝飾者模式。node

2 ESnext中的裝飾器模式

ESnext中有一個Decorator提案,使用一個以 @ 開頭的函數對ES6中的class及其屬性、方法進行修飾。Decorator的詳細語法請參考阮一峯的《ECMASciprt入門 —— Decorator》webpack

目前Decorator的語法還只是一個提案,若是指望如今使用裝飾器模式,須要安裝配合babel + webpack並結合插件實現。git

  • npm安裝依賴
npm install babel-core babel-loader babel-plugin-transform-decorators babel-plugin-transform-decorators-legacy babel-preset-env
  • 配置.babelrc文件
{
  "presets": ["env"],
  "plugins": ["transform-decorators-legacy"]
}
  • webpack.config.js中添加babel-loader
module: {
    rules: [
      { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
    ],
  }

若是你使用的IDE爲Visual Studio Code,可能還須要在項目根目錄下添加如下tsconfig.json文件來組織一個ts檢查的報錯。es6

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "allowJs": true,
    "lib": [
      "es6"
    ],
  }
}

下面我將實現3個裝飾器,分別爲@autobind@debounce@deprecategithub

2.1 @autobind實現this指向原對象

在JavaScript中,this的指向問題一直是一個老生常談的話題,在Vue或React這類框架的使用過程當中,新手頗有可能一不當心就丟失了this的指向致使方法調用錯誤。例以下面一段代碼:web

class Person {
  getPerson() {
    return this;
  }
}

let person = new Person();
let { getPerson } = person;

console.log(getPerson() === person); // false

上面的代碼中, getPerson方法中的this默認指向Person類的實例,可是若是將Person經過解構賦值的方式提取出來,那麼此時的this指向爲undefined。因此最終的打印結果爲falseajax

此時咱們能夠實現一個autobind的函數,用來裝飾getPerson這個方法,實現this永遠指向Person的實例。npm

function autobind(target, key, descriptor) {
  var fn = descriptor.value;
  var configurable = descriptor.configurable;
  var enumerable = descriptor.enumerable;

  // 返回descriptor
  return {
    configurable: configurable,
    enumerable: enumerable,
    get: function get() {
      // 將該方法綁定this
      var boundFn = fn.bind(this);
      // 使用Object.defineProperty從新定義該方法
      Object.defineProperty(this, key, {
        configurable: true,
        writable: true,
        enumerable: false,
        value: boundFn
      })

      return boundFn;
    }
  }
}

咱們經過bind實現了this的綁定,並在get中利用Object.defineProperty重寫了該方法,將value定義爲經過bind綁定後的函數boundFn,以此實現了this永遠指向實例。下面咱們爲getPerson方法加上裝飾並調用。json

class Person {
  @autobind
  getPerson() {
    return this;
  }
}

let person = new Person();
let { getPerson } = person;

console.log(getPerson() === person); // true

2.2 @debounce實現函數防抖

函數防抖(debounce)在前端項目中有着不少的應用,例如在resizescroll等事件中操做DOM,或對用戶輸入實現實時ajax搜索等會被高頻的觸發,前者會對瀏覽器性能產生直觀的影響,後者會對服務器產生較大的壓力,咱們指望這類高頻連續觸發的事件在觸發結束後再作出響應,這就是函數防抖的應用。

class Editor {
  constructor() {
    this.content = '';
  }

  updateContent(content) {
    console.log(content);
    this.content = content;
    // 後面有一些消耗性能的操做
  }
}

const editor1 = new Editor();
editor1.updateContent(1);
setTimeout(() => { editor1.updateContent(2); }, 400);


const editor2= new Editor();
editor2.updateContent(3);
setTimeout(() => { editor2.updateContent(4); }, 600);

// 打印結果: 1 3 2 4

上面的代碼中咱們定義了Editor這個類,其中updateContent方法會在用戶輸入時執行並可能有一些消耗性能的DOM操做,這裏咱們在該方法內部打印了傳入的參數以驗證調用過程。能夠看到4次的調用結果分別爲1 3 2 4

下面咱們實現一個debounce函數,該方法傳入一個數字類型的timeout參數。

function debounce(timeout) {
  const instanceMap = new Map(); // 建立一個Map的數據結構,將實例化對象做爲key

  return function (target, key, descriptor) {

    return Object.assign({}, descriptor, {
      value: function value() {

        // 清除延時器
        clearTimeout(instanceMap.get(this));
        // 設置延時器
        instanceMap.set(this, setTimeout(() => {
          // 調用該方法
          descriptor.value.apply(this, arguments);
          // 將延時器設置爲 null
          instanceMap.set(this, null);
        }, timeout));
      }
    })
  }
}

上面的方法中,咱們採用了ES6提供的Map數據結構去實現實例化對象和延時器的映射。在函數的內部,首先清除延時器,接着設置延時執行函數,這是實現debounce的通用方法,下面咱們來測試一下debounce裝飾器。

class Editor {
  constructor() {
    this.content = '';
  }

  @debounce(500)  
  updateContent(content) {
    console.log(content);
    this.content = content;
  }
}

const editor1 = new Editor();
editor1.updateContent(1);
setTimeout(() => { editor1.updateContent(2); }, 400);


const editor2= new Editor();
editor2.updateContent(3);
setTimeout(() => { editor2.updateContent(4); }, 600);

//打印結果: 3 2 4

上面調用了4次updateContent方法,打印結果爲3 2 41因爲在400ms內被重複調用而沒有被打印,這符合咱們的參數爲500的預期。

2.3 @deprecate實現警告提示

在使用第三方庫的過程當中,咱們會時不時的在控制檯碰見一些警告,這些警告用來提醒開發者所調用的方法會在下個版本中被棄用。這樣的一行打印信息也許咱們的常規作法是在方法內部添加一行代碼便可,這樣其實在源碼閱讀上並不友好,也不符合單一職責原則。若是在須要拋出警告的方法前面加一個@deprecate的裝飾器來實現警告,會友好得多。

下面咱們來實現一個@deprecate的裝飾器,其實這類的裝飾器也能夠擴展成爲打印日誌裝飾器@log,上報信息裝飾器@fetchInfo等。

function deprecate(deprecatedObj) {

  return function(target, key, descriptor) {
    const deprecatedInfo = deprecatedObj.info;
    const deprecatedUrl = deprecatedObj.url;
    // 警告信息
    const txt = `DEPRECATION ${target.constructor.name}#${key}: ${deprecatedInfo}. ${deprecatedUrl ? 'See '+ deprecatedUrl + ' for more detail' : ''}`;
    
    return Object.assign({}, descriptor, {
      value: function value() {
        // 打印警告信息
        console.warn(txt);
        descriptor.value.apply(this, arguments);
      }
    })
  }
}

上面的deprecate函數接受一個對象參數,該參數分別有infourl兩個鍵值,其中info填入警告信息,url爲選填的詳情網頁地址。下面咱們來爲一個名爲MyLib的庫的deprecatedMethod方法添加該裝飾器吧!

class MyLib {
  @deprecate({
    info: 'The methods will be deprecated in next version', 
    url: 'http://www.baidu.com'
  })
  deprecatedMethod(txt) {
    console.log(txt)
  }
}

const lib = new MyLib();
lib.deprecatedMethod('調用了一個要在下個版本被移除的方法');
// DEPRECATION MyLib#deprecatedMethod: The methods will be deprecated in next version. See http://www.baidu.com for more detail
// 調用了一個要在下個版本被移除的方法

3 總結

經過ESnext中的裝飾器實現裝飾器模式,不只有爲類擴充功能的做用,並且在閱讀源碼的過程當中起到了提示做用。上面所舉到的例子只是結合裝飾器的新語法和裝飾器模式作了一個簡單封裝,請勿用於生產環境。若是你如今已經體會到了裝飾器模式的好處,並想在項目中大量使用,不妨看一下core-decorators這個庫,其中封裝了不少經常使用的裝飾器.

參考文獻

  1. IMWeb的前端博客:淺談JS中的裝飾器模式
  2. 淘寶前端團隊:ES7 Decorator 裝飾者模式
  3. 阮一峯:ECMAScript 6 入門
相關文章
相關標籤/搜索