裝飾者模式

在程序開發中,許多時候都並不但願某個類天生就很是龐大,一次性包含許多職責。那麼咱們就可使用裝飾者模式。裝飾者模式能夠動態地給某個對象添加一些額外的職責,而不會影響這個類中派生的其餘對象。javascript

在傳統面嚮對象語言中,給對象添加功能經常使用繼承的方式,可是繼承的方式並不靈活,還會帶來許多問題:一方面會致使超類和子類之間存在強耦合性,當超類改變時,子類也會隨之改變;另外一方面,繼承這種功能複用一般被稱爲「白箱複用」,「白箱」是相對可見性而言的,在繼承方式中,超類的內部細節是對子類可見的,繼承經常被認爲破壞了封裝性。java

使用繼承還會帶來另一些問題,在完成一些功能複用的同時,有可能建立出大量的子類,使子類的數量呈爆炸性增加。編程

這種給對象動態增長職責的方式稱爲裝飾者(decorator)模式。裝飾者模式可以在不改變對象自身的基礎上,在程序運行期間給對象動態的添加職責。跟繼承相比,裝飾者是一種更輕更靈活的作法,這是一種「即用即付」的方式。設計模式

1. 模擬傳統面嚮對象語言的裝飾者模式

首先要提出來的是,做爲一門解釋執行的語言,給 JavaScript 中的對象動態添加或者改變職責是一件再簡單不過的事情,雖然這種作法改動了對象自身,跟傳統定義中的裝飾者模式並不同,但這無疑更符合 JavaScript 的語言特點。代碼以下:app

var obj = {
    name: 'sven',
    address: '深圳市'
};
obj.address = obj.address + '福田區';

傳統面嚮對象語言中的裝飾者模式在 JavaScript 中的適用場景並很少,如上面代碼所示,一般咱們並不太介意改動對象自身。儘管如此,咱們仍是稍微模擬一下傳統面嚮對象語言中的裝飾者模式來實現。函數

假設咱們在編寫一個飛機大戰的遊戲,隨着經驗值的增長,咱們操做的飛機對象能夠升級成更厲害的飛機,一開始這些飛機只能發射普通的子彈,升到第二級時能夠發射導彈,升到第三級時能夠發射原子彈。測試

下面來看代碼實現,首先是原始的飛機類:this

var Plane = function () {};
Plane.prototype.fire = function () {
    console.log('發射普通子彈');
};

接下來增長兩個裝飾類,分別是導彈和原子彈:atom

var MissileDecorator = function (plane) {
    this.plane = plane;
};
MissileDecorator.prototype.fire = function () {
    this.plane.fire();
    console.log('發射導彈');
};

var AtoDecorator = function (plane) {
    this.plane = plane;
};
AtoDecorator.prototype.fire = function () {
    this.plane.fire();
    console.log('發射原子彈');
}

導彈類和原子彈類的構造函數都接受參數 plane 對象,而且保存好這個參數,在他們的 fire 方法中,除了執行自身的操做以外,還調用 plane 對象的 fire 方法。prototype

這種給對象動態增長職責的方式,並無真正的改動對象自身,而是將對象放入另外一個對象之中,這些對象以一條鏈的方式進行引用,造成一個聚合對象。這些對象都擁有相同的接口(fire 方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操做,隨後把請求轉發給鏈中的下一個對象。

由於裝飾者對象和它所裝飾的對象擁有一致的接口,因此它們對使用該對象的客戶來講是透明的,被裝飾的對象也並不須要瞭解它曾經被裝飾過,這種透明性使得咱們能夠遞歸的嵌套任意多個裝飾者對象,如圖:

最後看看測試結果:

var plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtoDecorator(plane);

plane.fire();  //輸出:發射普通子彈,發射導彈,發射原子彈

2. 裝飾者也是包裝器

在《設計模式》成書以前,GoF 原想把裝飾者(decorator)模式稱爲包裝器(wrapper)模式。

從功能上而言,decorator 能很好的描述這個模式,但從結構上看,wrapper 的說法更加貼切。裝飾者模式將一個對象嵌入另外一個對象之中,實際上至關於這個對象被另外一個對象包裝起來,造成一條包裝鏈。請求隨着這條鏈依次傳遞到全部的對象,每一個對象都有處理這條請求的機會,如圖:

3. 回到 JavaScript 的裝飾者

JavaScript 語言動態改變對象至關容易,咱們能夠直接改寫對象或者對象的某個方法,並不須要使用「類」來實現裝飾者模式,代碼以下:

var plane = {
    fire: function () {
        console.log('發射普通子彈');
    }
};
var missileDecorator = function () {
    console.log('發射導彈');
};
var atomDecorator = function () {
    console.log('發射原子彈');
};

var fire1 = plane.fire;    //保存原方法

plane.fire = function () { //改寫新方法
    fire1();
    missileDecorator();
};

var fire2 = plane.fire;

plane.fire = function () {
    fire2();
    atomDecorator();
};

plane.fire();  //輸出:發射普通子彈,發射導彈,發射原子彈

4. 裝飾函數

在 JavaScript 中,幾乎一切都是對象,其中函數又被稱爲一等對象。在平時的開發工做中,也許大部分時間都在和函數打交道。在 JavaScript 中能夠很方便的給某個對象擴張屬性和方法,但卻很難在不改動某個函數源代碼的狀況下,給該函數添加一些額外的功能。在代碼的運行期間,咱們很難切入某個函數的執行環境。

要想爲函數添加一些功能,最簡單粗暴的方式就是直接改寫該函數,但這是最差的辦法,直接違反了開放——封閉原則:

var a = function () {
    alert(1);
};
//改爲
a = function () {
    alert(1);
    alert(2);
};

不少時候咱們不想去碰原函數,也許原函數是由其餘同事編寫的,裏面的實現很是雜亂。甚至在一個古老的項目中,這個函數的源代碼被隱藏在一個咱們不肯碰觸的陰暗角落裏。如今須要一個辦法,在不改變函數源代碼的狀況下,能給函數增長功能,這正是開放——封閉原則給咱們指出的光明道路。

其實在第 3 節的代碼中,咱們已經找到了一種答案,經過保存原引用的方式就能夠改寫某個函數:

var a = function () {
    alert(1);
};
var _a = a;
a = function () {
    _a();
    alert(2);
};

這是實際開發中很常見的一種作法,好比咱們想給 window 綁定 onload 事件,可是不肯定這個事件是否是已經被其餘人綁定過,爲了不覆蓋掉以前的 window.onload 函數中的行爲,咱們通常都會先保存好原先的 window.onload ,把它放入新的 window.onload 裏執行:

window.onload = function () {
    alert(1);
};
var _onload = window.onload || function () {};  //若是沒有 window.onload 就綁定一個空函數
window.onload = function () {
    _onload();
    alert(2);
}

這樣的代碼固然是符合開放——封閉原則的,咱們在增長新功能的時候,確實沒有修改原來的 window.onload 代碼,可是這種方式存在如下兩個問題。

  • 必須維護 _onload 這個中間變量,雖然看起來並不起眼,但若是函數的裝飾鏈較長,或者須要裝飾的函數變多,這些中間變量的數量也會愈來愈多。
  • 其實還遇到了 this 被劫持的問題,在 window.onload 的例子中沒有這個煩惱,是由於調用普通函數 _onload 時, this 也指向 window ,跟調用 window.onload 時同樣(將一個方法傳遞給一個普通變量時,this 會丟失綁定的對象,在嚴格模式中,this 爲 undefined,不然將綁定爲 window 對象)。如今把 window.onload 換成 document.getElementById ,代碼以下:

    var _getElementById = document.getElementById;
    
      document.getElementById = function (id) {
          alert (1);
          return _getElementById(id);   //(1)
      };
    
      var button = document.getElementById('button');

執行這段代碼,咱們看到在彈出 alert(1) 以後,緊接着控制檯拋出了異常:

//輸出:TypeError: 'getElementById' called on an object that does not implement interface Document.

異常發生在 (1) 處的 _getElementById(id) 這句代碼中,此時 _getElementById 是一個全局函數,當調用一個全局函數時,this 是指向 window 的,而 document.getElementById 方法的內部實現須要使用 this 引用,this 在這個方法內預期是指向 document ,而不是 window ,這是錯誤發生的緣由,因此使用如今的方式給函數增長功能並不保險。

改進後的代碼能夠知足需求,咱們要手動把 document 看成上下文 this 傳入 _getElementById:

var _getElementById = document.getElementById;

document.getElementById = function (id) {
    alert (1);
    return _getElementById.call(document, id);
};

var button = document.getElementById('button');

但這樣作顯然很不方便,下面咱們引入 AOP(面向切面編程),來提供一種完美的方法給函數動態增長功能。

5. 用 AOP 裝飾函數

首先來瞧瞧這兩個方法:

Function.prototype.before = function (beforefn) {
    var _self = this;   //保存原函數的引用
    return function () {    //返回包含了原函數和新函數的「代理」函數
        beforefn.apply(this, arguments);    //執行新函數,且保證 this 不被劫持,新函數接受的參數也會原封不動的傳入原函數,新函數在原函數以前執行
        return _self.apply(this, arguments);    //執行原函數並返回原函數的執行結果,而且保證 this 不被劫持
    }
}
Function.prototype.after = function (afterfn) {
    var _self = this;
    return function () {
        var ret = _self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}

Function.prototype.before 接受一個函數看成參數,這個函數即爲新添加的函數,它裝載了新函數添加的功能代碼。

接下來把當前的 this 保存起來,這個 this 指向原函數,而後返回一個「代理」函數,這個「代理」函數只是結構上像代理而已,並不承擔代理的職責(好比控制對象的訪問等)。它的工做是把請求分別轉發給新添加的函數和原函數,且負責保證它們的執行順序,讓新添加的函數在原函數以前執行(前置裝飾),這樣就實現了動態的裝飾效果。

咱們注意到,經過 Function.prototype.apply 來動態傳入正確的 this,保證了函數在被裝飾以後,this 不會被劫持。

Function.prototype.after 的原理跟 Function.prototype.before 相似,惟一不一樣的地方在於然新添加的函數在原函數執行以後再執行。

下面來試試看 Function.prototype.before 的威力:

document.getElementById = document.getElementById.before(function () {
    alert(1);
});
var button = document.getElementById('button');

再回到 window.onload 的例子,看看用 Function.prototype.after 來增長新的 window.onload 事件是多麼簡單:

window.onload = function () {
    alert(1);
}
window.onload = (window.onload || function () {}).after(function () {
    alert(2);
}).after(function () {
    alert(3);
}).after(function () {
    alert(4);
});

值得提到的是,上面的 AOP 實現是在 Function.prototype 上添加的 before 和 after 方法,但許多人不喜歡這種污染原型的方式,那麼咱們能夠作一些變通,把原函數和新函數都做爲參數傳入 before 或者 after 方法:

var before = function (fn, beforefn) {
    return function () {
        beforefn.apply(this, arguments);
        return fn.apply(this, arguments);
    }
};

var a = before(function () { alert(3);}, function () { alert(2);});
a = before(a, function () { alert(1);});

a();

應該注意的是,由於函數經過 Function.prototype.before 或者 Function.prototype.after 被裝飾以後,返回的其實是一個新的函數,若是在原函數上保存了一些屬性,那麼這些屬性會丟失。代碼以下:

var func = function () {
    alert(1);
};
func.a = 'a';

func = func.after(function () { alert (2);});

alert(func.a);  //輸出:undefined

參考書目:《JavaScript設計模式與開發實踐》

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息