代理模式
- 虛擬代理圖片預加載
如果直接給某個 img 標簽節點設置 src 屬性,由于圖片過大或者網絡不佳,圖片的位置往往有段時間會是一片空白。常見的做法是先用一張loading 圖片占位,然后用異步的方式加載圖片,等圖片加載好了再把它填充到 img 節點里,這種場景就很適合使用虛擬代理。
var myImage = (function() {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
var proxyImage = (function() {
var img = new Image;
img.onload = function() {
//等真正圖片加載好了之后,再改變成真實圖片
myImage.setSrc(this.src);
}
return {
setSrc: function(src) {
//先設置一張緩存圖片
myImage.setSrc('img/loading.gif');
img.src = src;
}
}
})();
proxyImage.setSrc('img/1.jpg');
- 虛擬代理合并 HTTP 請求
假設我們在做一個文件同步的功能,當我們選中一個 checkbox 的時候,它對應的文件就會被同
步到另外一臺備用服務器上面。當我們選中 3 個 checkbox 的時候,依次往服務器發送了 3 次同步文件的請求。可以預見,如此頻繁的網絡請求將會帶來相當大的開銷。
解決方案是,我們可以通過一個代理函數 proxySynchronousFile 來收集一段時間之內的請求,最后一次性發送給服務器。比如我們等待 2 秒之后才把這 2 秒之內需要同步的文件 ID 打包發給服務器,如果不是對實時性要求非常高的系統, 2 秒的延遲不會帶來太大副作用,卻能大大減輕服務器的壓力。代碼如下:
var synchronousFile = function(id) {
console.log('開始同步文件, id 為: ' + id);
};
var proxySynchronousFile = (function() {
var cache = [], // 保存一段時間內需要同步的 ID
timer; // 定時器
return function(id) {
cache.push(id);
if(timer) { // 保證不會覆蓋已經啟動的定時器
return;
}
timer = setTimeout(function() {
synchronousFile(cache.join(',')); // 2 秒后向本體發送需要同
clearTimeout(timer); // 清空定時器
timer = null;
cache.length = 0; // 清空 ID 集合
}, 2000);
}
})();
var checkbox = document.getElementsByTagName('input');
for(var i = 0, c; c = checkbox[i++];) {
c.onclick = function() {
if(this.checked === true) {
proxySynchronousFile(this.id);
}
}
};
責任鏈模式
一系列可能會處理請求的對象被連接成一條鏈,請求在這些對象之間依次傳遞,直到遇到一個可以處理它的對象。請求發送者只需要知道鏈中的第一個節點。ps:可以將業務代碼從if/else中釋放出來,便于程序的擴展。
業務例子:公司針對支付過定金的用戶有一定的優惠政策。在正式購買后,已經支付過 500 元定金的用戶會收到 100 元的商城優惠券, 200 元定金的用戶可以收到 50 元的優惠券,而之前沒有支付定金的用戶只能進入普通購買模式,也就是沒有優惠券,且在庫存有限的情況下不一定保證能買到。
/*職責鏈 */
/* orderType:表示訂單類型(定金用戶或者普通購買用戶), code 的值為 1 的時候是 500 元
定金用戶,為 2 的時候是 200 元定金用戶,為 3 的時候是普通購買用戶。
? pay:表示用戶是否已經支付定金,值為 true 或者 false, 雖然用戶已經下過 500 元定金的
訂單,但如果他一直沒有支付定金,現在只能降級進入普通購買模式。
? stock:表示當前用于普通購買的手機庫存數量,已經支付過 500 元或者 200 元定金的用
戶不受此限制
*/
var order500 = function(orderType, pay, stock) {
if(orderType === 1 && pay === true) {
console.log('500 元定金預購,得到 100 優惠券');
} else {
return 'nextSuccessor'; // 我不知道下一個節點是誰,反正把請求往后面傳遞
}
};
var order200 = function(orderType, pay, stock) {
if(orderType === 2 && pay === true) {
console.log('200 元定金預購,得到 50 優惠券');
} else {
return 'nextSuccessor'; // 我不知道下一個節點是誰,反正把請求往后面傳遞
}
};
var orderNormal = function(orderType, pay, stock) {
if(stock > 0) {
console.log('普通購買,無優惠券');
} else {
console.log('手機庫存不足');
}
};
var Chain = function(fn) {
this.fn = fn;
this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor) {
return this.successor = successor;
};
Chain.prototype.passRequest = function() {
var ret = this.fn.apply(this, arguments);
if(ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
};
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
//然后指定節點在職責鏈中的順序:
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
chainOrder500.passRequest( 1, true, 500 ); // 輸出: 500 元定金預購,得到 100 優惠券
chainOrder500.passRequest( 2, true, 500 ); // 輸出: 200 元定金預購,得到 50 優惠券
chainOrder500.passRequest( 3, true, 500 ); // 輸出:普通購買,無優惠券
chainOrder500.passRequest( 1, false, 0 ); // 輸出:手機庫存不足
命令模式
- 撤銷和重做
- 播放和錄像功能
- 宏命令
中介者模式
試想一下這個場景:一個手機購買界面,有選擇手機顏色,大小,型號等下拉框及輸入框。每當用戶進行操作后,要實時回顯用戶的操作,并且在某一項未選擇的時候,確認按鈕應該是被禁用狀態。
如果把這些內容都寫在操作的回調函數中,比如:在選擇手機顏色onselect事件中,寫很多邏輯代碼。那這樣就不利于程序的擴展。在以后新增了某個選項,則需要在每個輸入框的回調函數中增加對此新增的下拉框的判斷。
所以我們應該把邏輯代碼分離出來。交由中介者去完成判斷。對于新增的業務,也只需要改中介者這個對象中的業務代碼。在所有輸入框或下拉框的回調事件中,我們只需向中介者發個請求,告訴它,我現在變化了。中介者接收到這個信號后,會自動做一系列的處理判斷。代碼如下:
var goods = { // 手機庫存
"red|32G": 3,
"red|16G": 0,
"blue|32G": 1,
"blue|16G": 6
};
var mediator = (function() {
var colorSelect = document.getElementById('colorSelect'),
memorySelect = document.getElementById('memorySelect'),
numberInput = document.getElementById('numberInput'),
colorInfo = document.getElementById('colorInfo'),
memoryInfo = document.getElementById('memoryInfo'),
numberInfo = document.getElementById('numberInfo'),
nextBtn = document.getElementById('nextBtn');
return {
changed: function(obj) {
var color = colorSelect.value, // 顏色
memory = memorySelect.value, // 內存
number = numberInput.value, // 數量
stock = goods[color + '|' + memory]; // 顏色和內存對應的手機庫存數量
if(obj === colorSelect) { // 如果改變的是選擇顏色下拉框
colorInfo.innerHTML = color;
} else if(obj === memorySelect) {
memoryInfo.innerHTML = memory;
} else if(obj === numberInput) {
numberInfo.innerHTML = number;
}
if(!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '請選擇手機顏色';
return;
}
if(!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '請選擇內存大小';
return;
}
if(((number - 0) | 0) !== number - 0) { // 輸入購買數量是否為正整數
nextBtn.disabled = true;
nextBtn.innerHTML = '請輸入正確的購買數量';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入購物車';
}
}
})();
// 事件函數:
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numberInput.oninput = function() {
mediator.changed(this);
};
發布-訂閱模式
適用場景:一個網站,有好幾個模塊都需要獲取用戶信息后,進行渲染顯示。在未用此種設計模式之前,我們會想著在獲取用戶信息的ajax成功回調函數中,寫代碼,渲染各個模塊。但是這樣做會導致,我們新增一個模塊,需要去修改之前ajax的回調函數,加入新增部分的處理。而一個團隊中,可能ajax回調函數部分是同事A做的,而新增模塊是同事B負責的,其實B具體要做什么事情,A并不需要知道,A只需要告訴你,我已經拿到用戶信息了,你們想干嘛就干嘛吧。那么這時候A提供出一個接口供其他需要它的人來訂閱,在A完成任務后,告訴之前這些跟它打過招呼的模塊,我把這些對你們有用信息傳給你們,具體后續的事情由你們自己來決定。這就是發布-訂閱模式。
組合模式
這種模式非常適用于樹形結構
- 掃描文件夾
/******************************* Folder ******************************/
var Folder = function (name) {
this.name = name;
this.files = [];
};
Folder.prototype.add = function (file) {
this.files.push(file);
};
Folder.prototype.scan = function () {
console.log('開始掃描文件夾: ' + this.name);
for (var i = 0, file, files = this.files; file = files[i++];) {
file.scan();
}
};
/******************************* File ******************************/
var File = function (name) {
this.name = name;
};
File.prototype.add = function () {
throw new Error('文件下面不能再添加文件');
};
File.prototype.scan = function () {
console.log('開始掃描文件: ' + this.name);
};
var folder = new Folder('學習資料');
var folder1 = new Folder('JavaScript');
var folder2 = new Folder('jQuery');
var file1 = new File('JavaScript 設計模式與開發實踐');
var file2 = new File('精通 jQuery');
var file3 = new File('重構與模式')
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
var folder3 = new Folder('Nodejs');
var file4 = new File('深入淺出 Node.js');
folder3.add(file4);
var file5 = new File('JavaScript 語言精髓與編程實踐');
folder.add(folder3);
folder.add(file5);
folder.scan();
裝飾者模式
- 裝飾方法
當我們在擴展方法的時候,更保險的方法是將原來的方法保留下來,在新的方法中再執行一次原來的方法。比如我們想給 window 綁定 onload 事件,但是又不確定這個事件是不是已經被其他人綁定過,為了避免覆蓋掉之前的 window.onload 函數中的行為,我們一般都會先保存好原先的 window.onload,把它放入新的 window.onload 里執行:如:
window.onload = function() {
alert(1);
}
var _onload = window.onload || function() {};
window.onload = function() {
_onload();
alert(2);
}
但是上面會存在一些問題。1、 例如丟失了this指向。2、 需維護_onload 這個中間變量,如果函數的裝飾鏈夠長,所需的中間變量越多。
- 面向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;
}
};