5 分鐘即可掌握的 JavaScript 裝飾者模式與 AOP

圖片描述

什麼是裝飾者模式

當我們拍了一張照片準備發朋友圈時,許多小夥伴會選擇給照片加上濾鏡。同一張照片、不同的濾鏡組合起來就會有不同的體驗。這裏實際上就應用了裝飾者模式:是通過濾鏡裝飾了照片。在不改變對象(照片)的情況下動態的爲其添加功能(濾鏡)。

需要注意的是:由於 JavaScript 語言動態的特性,我們很容易就能改變某個對象(JavaScript 中函數是一等公民)。但是我們要儘量避免直接改寫某個函數,這會導致代碼的可維護性、可擴展性變差,甚至會污染其他業務。

什麼是 AOP

想必大家對"餐前洗手、飯後漱口"都不陌生。這句標語其實就是 AOP 在生活中的例子:吃飯這個動作相當於切點,我們可以在這個切點前、後插入其它如洗手等動作。

AOP(Aspect-Oriented Programming):面向切面編程,是對 OOP 的補充。利用AOP可以對業務邏輯的各個部分進行隔離,也可以隔離業務無關的功能比如日誌上報、異常處理等,從而使得業務邏輯各部分之間的耦合度降低,提高業務無關的功能的複用性,也就提高了開發的效率。

在 JavaScript 中,我們可以通過裝飾者模式來實現 AOP,但是兩者並不是一個維度的概念。 AOP 是一種編程範式,而裝飾者是一種設計模式。

ES3 下裝飾者的實現

瞭解了裝飾者模式和 AOP 的概念之後,我們寫一段能夠兼容 ES3 的代碼來實現裝飾者模式:

// 原函數
var takePhoto =function(){
  console.log('拍照片');
}
// 定義 aop 函數
var after=function( fn, afterfn ){ 
  return function(){
    let res = fn.apply( this, arguments ); 
    afterfn.apply( this, arguments );
    return res;
  }
}
// 裝飾函數
var addFilter=function(){
  console.log('加濾鏡');
}
// 用裝飾函數裝飾原函數
takePhoto=after(takePhoto,addFilter);

takePhoto();

這樣我們就實現了抽離拍照與濾鏡邏輯,如果以後需要自動上傳功能,也可以通過aop函數after來添加。

ES5 下裝飾者的實現

在 ES5 中引入了Object.defineProperty,我們可以更方便的給對象添加屬性:

let takePhoto = function () {
    console.log('拍照片');
}
// 給 takePhoto 添加屬性 after
Object.defineProperty(takePhoto, 'after', {
    writable: true,
    value: function () {
        console.log('加濾鏡');
    },
});
// 給 takePhoto 添加屬性 before
Object.defineProperty(takePhoto, 'before', {
    writable: true,
    value: function () {
        console.log('打開相機');
    },
});
// 包裝方法
let aop = function (fn) {
    return function () {
        fn.before()
        fn()
        fn.after()
    }
}

takePhoto = aop(takePhoto)
takePhoto()

基於原型鏈和類的裝飾者實現

我們知道,在 JavaScript 中,函數也好,類也好都有着自己的原型,通過原型鏈我們也能夠很方便的動態擴展,以下是基於原型鏈的寫法:

class Test {
    takePhoto() {
        console.log('拍照');
  }
}
// after AOP
function around(target, action, fn) {
    let old = target.prototype[action];
  if (old) {
    target.prototype[action] = function () {
      let self = this;
      fn.bind(self);
      fn(handle);
    }
  }
}
// 用 AOP 函數修飾原函數
after(Test, 'takePhoto', () => {
  console.log('添加濾鏡');
});

let t = new Test();
t.takePhoto();

使用 ES7 修飾器實現裝飾者

在 ES7 中引入了@decorator 修飾器的提案,參考阮一峯的文章)。修飾器是一個函數,用來修改類的行爲。目前Babel轉碼器已經支持。注意修飾器只能裝飾類或者類屬性、方法。三者的具體區別請參考 MDN Object.defineProperty ;而 TypeScript 的實現又有所不同:TypeScript Decorator

接下來我們通過修飾器來實現對方法的裝飾:

function after(target, key, desc) {
  const { value } = desc;
  desc.value = function (...args) {
    let res = value.apply(this, args);
    console.log('加濾鏡')
    return res;
  }
  return desc;
}

class Test{
  @after
  takePhoto(){
    console.log('拍照')
  }
}

let t = new Test()
t.takePhoto()

可以看到,使用修飾器的代碼非常簡潔明瞭。

場景:性能上報

裝飾者模式可以應用在很多場景,典型的場景是記錄某異步請求請求耗時的性能數據並上報:

function report(target, key, desc) {
  const { value } = desc;
  desc.value = async function (...args) {
        let start = Date.now();
    let res = await value.apply(this, args);
        let millis = Date.now()-start;
        // 上報代碼
        return res;
  }
  return desc;
}

class Test{
    @report
  getData(url){
    // fetch 代碼
  }
}

let t = new Test()
t.getData()

這樣使用@report修飾後的代碼就會上報請求所消耗的時間。擴展或者修改report函數不會影響業務代碼,反之亦然。

場景:異常處理

我們可以對原有代碼進行簡單的異常處理,而無需侵入式的修改:

function handleError(target, key, desc) {
    const { value } = desc;
  desc.value = async function (...args) {
    let res;
    try{
      res = await value.apply(this, args);
    }catch(err){
      // 異常處理
      logger.error(err)
    }
    return res;
  }
  return desc;
}

class Test{
    @handleError
  getData(url){
    // fetch 代碼
  }
}

let t = new Test()
t.getData()

通過以上兩個示例我們可以看到,修飾器的定義很簡單,功能卻非常強大。

小結

我們一步一步通過高階函數、原型鏈、Object.defineProperty@Decorator分別實現了裝飾者模式。接下來在回顧一下:

  • 裝飾者模式非常適合給業務代碼附加非業務相關功能(如日誌上報),就如同給照片加濾鏡;
  • 裝飾者模式非常適合無痛擴展別人的代碼(你經常需要接手別人的項目吧)

有些朋友可能會覺得裝飾者模式和 vue 的 mixin 機制很像,其實他們都是“開放-封閉原則”和“單一職責原則”的體現。

附上代碼 jsbin 地址:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章