玩魔獸爭霸的任務關時,對 15 級亂加技能點的野生英雄廣泛沒有好感,而是喜歡留着
技能點,在遊戲的進行過程當中按需加技能。一樣,在程序開發中,許多時候都並不但願某個類天
生就很是龐大,一次性包含許多職責。那麼咱們就可使用裝飾者模式。裝飾者模式能夠動態地
給某個對象添加一些額外的職責,而不會影響從這個類中派生的其餘對象。
在傳統的面嚮對象語言中,給對象添加功能經常使用繼承的方式,可是繼承的方式並不靈活,
還會帶來許多問題:一方面會致使超類和子類之間存在強耦合性,當超類改變時,子類也會隨之
改變;另外一方面,繼承這種功能複用方式一般被稱爲「白箱複用」,「白箱」是相對可見性而言的,
在繼承方式中,超類的內部細節是對子類可見的,繼承經常被認爲破壞了封裝性。javascript
使用繼承還會帶來另一個問題,在完成一些功能複用的同時,有可能建立出大量的子類,
使子類的數量呈爆炸性增加。好比如今有 4種型號的自行車,咱們爲每種自行車都定義了一個單
獨的類。如今要給每種自行車都裝上前燈、尾
燈和鈴鐺這 3種配件。若是使用繼承的方式來給
每種自行車建立子類,則須要 4×3 = 12 個子類。
可是若是把前燈、尾燈、鈴鐺這些對象動態組
合到自行車上面,則只須要額外增長 3個類。java
這種給對象動態地增長職責的方式稱爲裝
飾者(decorator)模式。裝飾者模式可以在不改
變對象自身的基礎上,在程序運行期間給對象
動態地添加職責。跟繼承相比,裝飾者是一種
更輕便靈活的作法,這是一種「即用即付」的
方式,好比天冷了就多穿一件外套,須要飛行
時就在頭上插一支竹蜻蜓,遇到一堆食屍鬼時
就點開 AOE(範圍攻擊)技能。ajax
var obj = { name: 'sven', address: '深圳市' }; obj.address = obj.address + '福田區';
傳統面嚮對象語言中的裝飾者模式在 JavaScript中適用的場景並很少,如上面代碼所示,通
常咱們並不太介意改動對象自身。儘管如此,本節咱們仍是稍微模擬一下傳統面嚮對象語言中的
裝飾者模式實現。
假設咱們在編寫一個飛機大戰的遊戲,隨着經驗值的增長,咱們操做的飛機對象能夠升級成
更厲害的飛機,一開始這些飛機只能發射普通的子彈,升到第二級時能夠發射導彈,升到第三級
時能夠發射原子彈。
下面來看代碼實現,首先是原始的飛機類:設計模式
var Plane = function(){} Plane.prototype.fire = function(){ console.log( '發射普通子彈' ); }
接下來增長兩個裝飾類,分別是導彈和原子彈:app
var MissileDecorator = function( plane ){ this.plane = plane; } MissileDecorator.prototype.fire = function(){ this.plane.fire(); console.log( '發射導彈' ); } var AtomDecorator = function( plane ){ this.plane = plane; } AtomDecorator.prototype.fire = function(){ this.plane.fire(); console.log( '發射原子彈' ); }
導彈類和原子彈類的構造函數都接受參數 plane 對象,而且保存好這個參數,在它們的 fire
方法中,除了執行自身的操做以外,還調用 plane 對象的 fire 方法。
這種給對象動態增長職責的方式,並無真正地改動對象自身,而是將對象放入另外一個對象
之中,這些對象以一條鏈的方式進行引用,造成一個聚合對象。這些對象都擁有相同的接口( fire
方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操做,隨後把請求轉發給鏈中的
下一個對象。框架
由於裝飾者對象和它所裝飾的對象擁有一致的接口,因此它們對使用該對象的客戶來講是透
明的,被裝飾的對象也並不須要瞭解它曾經被裝飾過,這種透明性使得咱們能夠遞歸地嵌套任意
多個裝飾者對象。異步
最後看看測試結果:函數
var plane = new Plane(); plane = new MissileDecorator( plane ); plane = new AtomDecorator( plane ); plane.fire(); // 分別輸出: 發射普通子彈、發射導彈、發射原子彈
在《設計模式》成書以前,GoF原想把裝
飾者(decorator)模式稱爲包裝器(wrapper)
模式。
從功能上而言,decorator能很好地描述這
個模式,但從結構上看,wrapper 的說法更加
貼切。裝飾者模式將一個對象嵌入另外一個對象
之中,實際上至關於這個對象被另外一個對象包
裝起來,造成一條包裝鏈。請求隨着這條鏈依
次傳遞到全部的對象,每一個對象都有處理這條
請求的機會。性能
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(); // 分別輸出: 發射普通子彈、發射導彈、發射原子彈
在 JavaScript中,幾乎一切都是對象,其中函數又被稱爲一等對象。在平時的開發工做中,
也許大部分時間都在和函數打交道。在 JavaScript中能夠很方便地給某個對象擴展屬性和方法,
但卻很難在不改動某個函數源代碼的狀況下,給該函數添加一些額外的功能。在代碼的運行期間,
咱們很難切入某個函數的執行環境。
要想爲函數添加一些功能,最簡單粗暴的方式就是直接改寫該函數,但這是最差的辦法,直
接違反了開放封閉原則:測試
var a = function(){ alert (1); } // 改爲: var a = function(){ alert (1); alert (2); }
不少時候咱們不想去碰原函數,也許原函數是由其餘同事編寫的,裏面的實現很是雜亂。甚
至在一個古老的項目中,這個函數的源代碼被隱藏在一個咱們不肯碰觸的陰暗角落裏。如今須要
一個辦法,在不改變函數源代碼的狀況下,能給函數增長功能,這正是開放封閉原則給咱們指
出的光明道路。
其實在 3節的代碼中,咱們已經找到了一種答案,經過保存原引用的方式就能夠改寫某個
函數:
var a = function(){ alert (1); } var _a = a; a = function(){ _a(); alert (2); } a();
這是實際開發中很常見的一種作法,好比咱們想給 window 綁定 onload 事件,可是又不肯定
這個事件是否是已經被其餘人綁定過,爲了不覆蓋掉以前的 window.onload 函數中的行爲,我
們通常都會先保存好原先的 window.onload ,把它放入新的 window.onload 裏執行:
window.onload = function(){ alert (1); } var _onload = window.onload || function(){}; window.onload = function(){ _onload(); alert (2); }
必須維護 _onload 這個中間變量,雖然看起來並不起眼,但若是函數的裝飾鏈較長,或者
須要裝飾的函數變多,這些中間變量的數量也會愈來愈多。
其實還遇到了 this 被劫持的問題,在 window.onload 的例子中沒有這個煩惱,是由於調用
普通函數 _onload 時, this 也指向 window ,跟調用 window.onload 時同樣(函數做爲對象的
方法被調用時, this 指向該對象,因此此處 this 也只指向 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) 以後,緊接着控制檯拋出了異常:
// 輸出: Uncaught TypeError: Illegal invocation
異常發生在(1) 處的 _getElementById( id ) 這句代碼上,此時 _getElementById 是一個全局函數,
當調用一個全局函數時, this 是指向 window 的,而 document.getElementById 方法的內部實現須要
使用 this 引用, this 在這個方法內預期是指向 document ,而不是 window , 這是錯誤發生的緣由,
因此使用如今的方式給函數增長功能並不保險。
改進後的代碼能夠知足需求,咱們要手動把 document 看成上下文 this 傳入 _getElementById :
var _getElementById = document.getElementById;
document.getElementById = function(){
alert (1);
return _getElementById.apply( document, arguments );
}
var button = document.getElementById( 'button' );
但這樣作顯然很不方便,下面咱們用 AOP,來提供一種完美的方法給
函數動態增長功能。
首先給出 Function.prototype.before 方法和 Function.prototype.after 方法:
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 的威力:
<button id="button"></button> Function.prototype.before = function( beforefn ){ var __self = this; return function(){ beforefn.apply( this, arguments ); return __self.apply( this, arguments ); } } document.getElementById = document.getElementById.before(function(){ alert (1); }); var button = document.getElementById( 'button' ); console.log( button );
再回到 window.onload 的例子,看看用 Function.prototype.before 來增長新的 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();
用 AOP裝飾函數的技巧在實際開發中很是有用。不管是業務代碼的編寫,仍是在框架層面,
咱們均可以把行爲依照職責分紅粒度更細的函數,隨後經過裝飾把它們合併到一塊兒,這有助於我
們編寫一個鬆耦合和高複用性的系統。
介紹幾個例子,帶你們進一步理解裝飾函數的威力。
1 數據統計上報
分離業務代碼和數據統計代碼,不管在什麼語言中,都是 AOP的經典應用之一。在項目開發
的結尾階段不免要加上不少統計數據的代碼,這些過程可能讓咱們被迫改動早已封裝好的函數。
好比頁面中有一個登陸 button,點擊這個 button會彈出登陸浮層,與此同時要進行數據上報,
來統計有多少用戶點擊了這個登陸 button:
<button tag="login" id="button">點擊打開登陸浮層</button> var showLogin = function(){ console.log( '打開登陸浮層' ); log( this.getAttribute( 'tag' ) ); } var log = function( tag ){ console.log( '上報標籤爲: ' + tag ); // (new Image).src = 'http:// xxx.com/report?tag=' + tag; // 真正的上報代碼略 } document.getElementById( 'button' ).onclick = showLogin;
咱們看到在 showLogin 函數裏,既要負責打開登陸浮層,又要負責數據上報,這是兩個層面
的功能,在此處卻被耦合在一個函數裏。使用 AOP分離以後,代碼以下:
Function.prototype.after = function( afterfn ){ var __self = this; return function(){ var ret = __self.apply( this, arguments ); afterfn.apply( this, arguments ); return ret; } }; var showLogin = function(){ console.log( '打開登陸浮層' ); } var log = function(){ console.log( '上報標籤爲: ' + this.getAttribute( 'tag' ) ); } showLogin = showLogin.after( log ); // 打開登陸浮層以後上報數據 document.getElementById( 'button' ).onclick = showLogin; 咱們看到在 showLogin 函數裏,既要負責打開登陸浮層,又要負責數據上報,這是兩個層面 的功能,在此處卻被耦合在一個函數裏。使用 AOP分離以後,代碼以下: Function.prototype.after = function( afterfn ){ var __self = this; return function(){ var ret = __self.apply( this, arguments ); afterfn.apply( this, arguments ); return ret; } }; var showLogin = function(){ console.log( '打開登陸浮層' ); } var log = function(){ console.log( '上報標籤爲: ' + this.getAttribute( 'tag' ) ); } showLogin = showLogin.after( log ); // 打開登陸浮層以後上報數據 document.getElementById( 'button' ).onclick = showLogin;
2 用AOP動態改變函數的參數
觀察 Function.prototype.before 方法:
Function.prototype.before = function( beforefn ){ var __self = this; return function(){ beforefn.apply( this, arguments ); // (1) return __self.apply( this, arguments ); // (2) } }
從這段代碼的(1)處和(2)處能夠看到, beforefn 和原函數 __self 共用一組參數列表
arguments ,當咱們在 beforefn 的函數體內改變 arguments 的時候,原函數 __self 接收的參數列
表天然也會變化。
下面的例子展現瞭如何經過 Function.prototype.before 方法給函數 func 的參數 param 動態地
添加屬性 b :
var func = function( param ){ console.log( param ); // 輸出: {a: "a", b: "b"} } func = func.before( function( param ){ param.b = 'b'; }); func( {a: 'a'} );
如今有一個用於發起 ajax請求的函數,這個函數負責項目中全部的 ajax異步請求:
var ajax = function( type, url, param ){ console.dir(param); // 發送 ajax 請求的代碼略 }; ajax( 'get', 'http:// xxx.com/userinfo', { name: 'sven' } );
上面的僞代碼表示向後臺 cgi 發起一個請求來獲取用戶信息,傳遞給 cgi 的參數是 { name:
'sven' } 。
ajax 函數在項目中一直運轉良好,跟 cgi 的合做也很愉快。直到有一天,咱們的網站遭受了
CSRF攻擊。解決 CSRF攻擊最簡單的一個辦法就是在 HTTP請求中帶上一個 Token 參數。
.
var getToken = function(){ return 'Token'; } 如今的任務是給每一個 ajax請求都加上 Token 參數: var ajax = function( type, url, param ){ param = param || {}; Param.Token = getToken(); // 發送 ajax 請求的代碼略... };
雖然已經解決了問題,但咱們的 ajax 函數相對變得僵硬了,每一個從 ajax 函數裏發出的請求
都自動帶上了 Token 參數,雖然在如今的項目中沒有什麼問題,但若是未來把這個函數移植到其
他項目上,或者把它放到一個開源庫中供其餘人使用, Token 參數都將是多餘的。
也許另外一個項目不須要驗證 Token ,或者是 Token 的生成方式不一樣,不管是哪一種狀況,都必
須從新修改 ajax 函數。
爲了解決這個問題,先把 ajax 函數還原成一個乾淨的函數:
var ajax= function( type, url, param ){ console.log(param); // 發送 ajax 請求的代碼略 }; 而後把 Token 參數經過 Function.prototyte.before 裝飾到 ajax 函數的參數 param 對象中: var getToken = function(){ return 'Token'; } ajax = ajax.before(function( type, url, param ){ param.Token = getToken(); }); ajax( 'get', 'http:// xxx.com/userinfo', { name: 'sven' } ); 從 ajax 函數打印的 log能夠看到, Token 參數已經被附加到了 ajax 請求的參數中: {name: "sven", Token: "Token"}
明顯能夠看到,用 AOP 的方式給 ajax 函數動態裝飾上 Token 參數,保證了 ajax 函數是一
個相對純淨的函數,提升了 ajax 函數的可複用性,它在被遷往其餘項目的時候,不須要作任何
修改。
咱們不少人都寫過許多表單驗證的代碼,在一個 Web 項目中,可能存在很是多的表單,如
註冊、登陸、修改用戶信息等。在表單數據提交給後臺以前,經常要作一些校驗,好比登陸的時
候須要驗證用戶名和密碼是否爲空,代碼以下:
用戶名:<input id="username" type="text"/> 密碼: <input id="password" type="password"/> <input id="submitBtn" type="button" value="提交"> var username = document.getElementById( 'username' ), password = document.getElementById( 'password' ), submitBtn = document.getElementById( 'submitBtn' ); var formSubmit = function(){ if ( username.value === '' ){ return alert ( '用戶名不能爲空' ); } if ( password.value === '' ){ return alert ( '密碼不能爲空' ); } var param = { username: username.value, password: password.value } ajax( 'http:// xxx.com/login', param ); // ajax 具體實現略 } submitBtn.onclick = function(){ formSubmit(); }
formSubmit 函數在此處承擔了兩個職責,除了提交 ajax請求以外,還要驗證用戶輸入的合法
性。這種代碼一來會形成函數臃腫,職責混亂,二來談不上任何可複用性。
本節的目的是分離校驗輸入和提交 ajax 請求的代碼,咱們把校驗輸入的邏輯放到 validata
函數中,而且約定當 validata 函數返回 false 的時候,表示校驗未經過,代碼以下:
var validata = function(){ if ( username.value === '' ){ alert ( '用戶名不能爲空' ); return false; } if ( password.value === '' ){ alert ( '密碼不能爲空' ); return false; } } var formSubmit = function(){ if ( validata() === false ){ // 校驗未經過 return; } var param = { username: username.value, password: password.value } ajax( 'http:// xxx.com/login', param ); } submitBtn.onclick = function(){ formSubmit(); }
如今的代碼已經有了一些改進,咱們把校驗的邏輯都放到了 validata 函數中,但 formSubmit
函數的內部還要計算 validata 函數的返回值,由於返回值的結果代表了是否經過校驗。
接下來進一步優化這段代碼,使 validata 和 formSubmit 徹底分離開來。首先要改寫 Function.
prototype.before , 若是 beforefn 的執行結果返回 false ,表示再也不執行後面的原函數,代碼以下:
Function.prototype.before = function( beforefn ){ var __self = this; return function(){ if ( beforefn.apply( this, arguments ) === false ){ // beforefn 返回 false 的狀況直接 return,再也不執行後面的原函數 return; } return __self.apply( this, arguments ); } } var validata = function(){ if ( username.value === '' ){ alert ( '用戶名不能爲空' ); return false; } if ( password.value === '' ){ alert ( '密碼不能爲空' ); return false; } } formSubmit = formSubmit.before( validata ); submitBtn.onclick = function(){ formSubmit(); }
在這段代碼中,校驗輸入和提交表單的代碼徹底分離開來,它們再也不有任何耦合關係,
formSubmit = formSubmit.before( validata ) 這句代碼,如同把校驗規則動態接在 formSubmit 函數
以前, validata 成爲一個即插即用的函數,它甚至能夠被寫成配置文件的形式,這有利於咱們分
開維護這兩個函數。再利用策略模式稍加改造,咱們就能夠把這些校驗規則都寫成插件的形式,
用在不一樣的項目當中。
var func = function(){ alert( 1 ); } func.a = 'a'; func = func.after( function(){ alert( 2 ); }); alert ( func.a ); // 輸出:undefined
另外,這種裝飾方式也疊加了函數的做用域,若是裝飾的鏈條過長,性能上也會受到一些
影響。
裝飾者模式和
代理模式的結構看起來很是相像,這兩種模式都描述了怎樣爲對象提供
必定程度上的間接引用,它們的實現部分都保留了對另一個對象的引用,而且向那個對象發送
請求。
代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當直
接訪問本體不方便或者不符合須要時,爲這個本體提供一個替代者。本體定義了關鍵功能,而代
理提供或拒絕對它的訪問,或者在訪問本體以前作一些額外的事情。裝飾者模式的做用就是爲對
象動態加入行爲。換句話說,代理模式強調一種關係(Proxy與它的實體之間的關係),這種關係
能夠靜態的表達,也就是說,這種關係在一開始就能夠被肯定。而裝飾者模式用於一開始不能確
定對象的所有功能時。代理模式一般只有一層代理本體的引用,而裝飾者模式常常會造成一條
長長的裝飾鏈。
在虛擬代理實現圖片預加載的例子中,本體負責設置 img 節點的 src,代理則提供了預加載
的功能,這看起來也是「加入行爲」的一種方式,但這種加入行爲的方式和裝飾者模式的偏重點
是不同的。裝飾者模式是實實在在的爲對象增長新的職責和行爲,而代理作的事情仍是跟本體
同樣,最終都是設置 src。但代理能夠加入一些「聰明」的功能,好比在圖片真正加載好以前,
先使用一張佔位的 loading圖片反饋給客戶。
小結
經過數據上報、統計函數的執行時間、動態改變函數參數以及插件式的表單驗證這 4個 例子,咱們瞭解了裝飾函數,它是 JavaScript中獨特的裝飾者模式。這種模式在實際開發中很是 有用,除了上面提到的例子,它在框架開發中也十分有用。做爲框架做者,咱們但願框架裏的函 數提供的是一些穩定而方便移植的功能,那些個性化的功能能夠在框架以外動態裝飾上去,這可 以免爲了讓框架擁有更多的功能,而去使用一些 if 、 else 語句預測用戶的實際須要。