裝飾者模式
在程序開發中,許多時候都並不希望某個類天生就非常龐大,一次性包含許多職責。那麼我們就可以使用裝飾者模式。裝飾者模式可以動態地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其他對象。
level01:模擬傳統面嚮對象語言的裝飾者模式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var Plane = function(){}
Plane.prototype.fire = function(){
console.log( '發射普通子彈' );
}
// 接下來增加兩個裝飾類,分別是導彈和原子彈:
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( '發射原子彈' );
}
var plane = new Plane();
plane = new MissileDecorator( plane );
plane = new AtomDecorator( plane );
plane.fire();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈
</script>
</body>
</html>
導彈類和原子彈類的構造函數都接受參數 plane 對象,並且保存好這個參數,在它們的 fire方法中,除了執行自身的操作之外,還調用 plane 對象的 fire 方法。
這種給對象動態增加職責的方式,並沒有真正地改動對象自身,而是將對象放入另一個對象之中,這些對象以一條鏈的方式進行引用,形成一個聚合對象。這些對象都擁有相同的接口( fire
方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操作,隨後把請求轉發給鏈中的
下一個對象。
因爲裝飾者對象和它所裝飾的對象擁有一致的接口,所以它們對使用該對象的客戶來說是透明的,被裝飾的對象也並不需要了解它曾經被裝飾過,這種透明性使得我們可以遞歸地嵌套任意多個裝飾者對象。
level02:回到 JavaScript 的裝飾者
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
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();
}
//前者plane.fire()嵌套在後者plane.fire()裏
var fire2 = plane.fire;
plane.fire = function(){
fire2();
atomDecorator();
}
plane.fire();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈
</script>
</body>
</html>
level01:裝飾函數
var a = function(){
alert (1);
}
// 改成:
var a = function(){
alert (1);
alert (2);
}
缺點:直接違反了開放封閉原則。
level02:裝飾函數
很多時候我們不想去碰原函數,也許原函數是由其他同事編的,裏面的實現非常雜亂。甚至在一個古老的項目中,這個函數的源代碼被隱藏在一個我們不願碰觸的陰暗角落裏。現在需要一個辦法,在不改變函數源代碼的情況下,能給函數增加功能,這正是開放-封閉原則給我們指出的光明道路。
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);
}
這樣的代碼當然是符合開放封閉原則的,我們在增加新功能的時候,確實沒有修改原來的window.onload 代碼,但是這種方式存在以下兩個問題。
- 必須維護 _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 :
<html>
<button id="button"></button>
<script>
var _getElementById = document.getElementById;
document.getElementById = function(){
alert (1);
return _getElementById.apply( document, arguments );
}
var button = document.getElementById( 'button' );
</script>
</html>
裝飾者模式和代理模式
裝飾者模式和代理模式的結構看起來非常相像,這兩種模式都描述了怎樣爲對象提供一定程度上的間接引用,它們的實現部分都保留了對另外一個對象的引用,並且向那個對象發送請求。
代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當接訪問本體不方便或者不符合需要時,爲這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情。裝飾者模式的作用就是爲對象動態加入行爲。換句話說,代理模式強調一種關係(Proxy與它的實體之間的關係),這種關係可以靜態的表達,也就是說,這種關係在一開始就可以被確定。而裝飾者模式用於一開始不能確定對象的全部功能時。代理模式通常只有一層代理本體的引用,而裝飾者模式經常會形成一條長長的裝飾鏈。
在虛擬代理實現圖片預加載的例子中,本體負責設置 img 節點的 src,代理則提供了預加載的功能,這看起來也是“加入行爲”的一種方式,但這種加入行爲的方式和裝飾者模式的偏重點
是不一樣的。裝飾者模式是實實在在的爲對象增加新的職責和行爲,而代理做的事情還是跟本體一樣,最終都是設置 src。但代理可以加入一些“聰明”的功能,比如在圖片真正加載好之前,
先使用一張佔位的 loading圖片反饋給客戶。