《JavaScript設計模式與開發實踐》14種設計模式學習筆記。
單例模式
保證一個類僅有一個例項,並提供一個全域性訪問點。
// 建構函式
var Singleton = function (name) {
this.name = name;
}
Singleton.prototype.getName = function () {
alert(this.name);
}
// 方案1 繫結到建構函式上
Singleton.instance = null;
Singleton.getInstance = function (name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
// 方案2 閉包
Singleton.getInstance = (function () {
var instance = null;
return function (name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})()
惰性單例
需要的時候才建立物件例項
var getSingle = function (fn) {
var result;
return function () {
return result || (result = fn.apply(this, arguments));
}
}
例如一個Modal彈框,頁面載入時不需要建立,按鈕點選後才會被建立。以後再點選按鈕不需要建立新的Modal。
var createModal = function () {
var div = document.createElement('div');
div.style.display = 'none';
document.body.append(div);
return div;
}
createSingleModal = getSingle(createModal);
btn.click = function () {
var modal = createSingleModal();
modal.style.display = 'block';
}
策略模式
定義一系列演算法,把它們一個個封裝起來,並且使它們可以互相替換。在實際開發中,我們通常會把演算法的含義擴散開來,使用策略模式也可以用來封裝一系列目標一致的‘業務規則’。
例如計算年終獎:
var strategies = {
"S": function (salary) {
return salary * 4;
},
"A": function (salary) {
return salary * 3;
},
"B": function (salary) {
return salary * 2;
},
}
var calculateBonus = function (level, salary) {
return strategies[level](salary);
}
calculateBonus('S', 20000); // 80000
calculateBonus('A', 10000); // 30000
代理模式
顧名思義,代理。
當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。
虛擬代理吧一些開銷很大的物件,延遲到真正需要他的時候才去建立。
虛擬代理實現圖片預載入
var myImage = (function () {
var imgNode = document.createElement('img');
document.body.append(imgNode);
return function (src) {
imgNode.src = src;
}
})()
// 代理 myImage,實現預載入
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage(this.src);
}
return function (src) {
myImage('./loading.gif');
img.src = src;
}
})()
proxyImage('http://xxx.10M.png');
JavaScript 開發中最常用的是虛擬代理和快取代理。
迭代器模式
迭代器模式是指提供一種方法順序訪問一個聚合物件中的各個元素,而又不需要暴露該物件的內部表示。
實現迭代器(內部迭代器)
var each = function (arr, callback) {
for (var i = 0; i < arr.length; i++) {
if (callback.call(arr[i], arr[i], i) === false) {
break;
}
}
}
each([1, 2, 3, 4, 5], function (item, index) {
if (index > 3) {
return false;
}
console.log(item, index);
})
外部迭代器
var Iterator = function (obj) {
var current = 0;
var next = function () {
current += 1;
};
var isDone = function () {
return current >= obj.length;
};
var getCurrItem = function () {
return obj[current];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem,
length: obj.length,
}
}
釋出-訂閱模式
定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。
var event = {
clientList: {},
listen: function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
},
trigger: function () {
var key = Array.prototype.shift.apply(arguments),
fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0; i < fns.length; i++) {
fns[i].apply(this, arguments);
}
},
remove: function (key, fn) {
var fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
if (!fn) {
this.clientList = [];
} else {
for (var i = 0; i < fns.length; i++) {
if (fn === fns[i]) {
fns.splice(i, 1);
}
}
}
},
}
DOM事件也是釋出-訂閱模式。
命令模式
命令模式把程式碼封裝成命令,目的解藕。
命令模式有 接收者receiver,執行方法execute;execute 去執行 receiver.xxx()。
var setCommand = function (button, command) {
button.onClick = function () {
command.execute();
}
}
var MenuBar = {
refresh: function () {
console.log('重新整理選單')
}
}
var RefreshMenuBarCommand = function (receiver) {
this.receiver = receiver;
}
RefreshMenuBarCommand.prototype.execute = function () {
this.receiver.refresh();
}
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
setCommand(btn, refreshMenuBarCommand);
JavaScript 中的命令模式未必要使用物件導向:
var setCommand = function (button, command) {
button.onClick = function () {
command.execute();
}
}
var MenuBar = {
refresh: function () {
console.log('重新整理選單');
}
}
var RefreshMenuBarCommand = function (receiver) {
return {
execute: function () {
receiver.refresh();
}
}
}
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(btn, refreshMenuBarCommand);
JavaScript 中命令模式還可以不需要接收者:
var refreshMenuBarCommand = {
execute: function () {
console.log('重新整理選單');
}
};
這樣看起來程式碼結構和策略模式非常相近,但他們的意圖不同,策略模式的策略物件的目標總是一致的,命令模式的目標更具散發性,命令模式還可以完成撤銷,排隊等功能。
組合模式
組合模式中有兩個名詞:組合物件,葉物件。
結構如圖
組合模式的例子-掃描資料夾
// Folder
var Folder = function (name) {
this.name = name;
this.parent = null;
this.files = [];
};
Folder.prototype.add = function (file) {
file.parent = this;
this.files.push(file);
};
Folder.prototype.remove = function () {
if (!this.parent) {
return;
}
for (var files = this.parent.files, i = 0; i < files.length; i++) {
var file = files[i];
if (file === this) {
files.splice(i, 1);
break;
}
}
};
Folder.prototype.scan = function () {
console.log('開始掃描資料夾:' + this.name);
for (var i = 0; i < this.files.length; i++) {
var file = this.files[i];
file.scan();
}
};
// File
var File = function (name) {
this.name = name;
this.parent = null;
};
File.prototype.add = function () {
throw new Error('資料夾下面不能在新增檔案');
};
Folder.prototype.remove = function () {
if (!this.parent) {
return;
}
for (var files = this.parent.files, i = 0; i < files.length; i++) {
var file = files[i];
if (file === this) {
files.splice(i, 1);
break;
}
}
};
File.prototype.scan = function () {
console.log('開始掃描檔案:' + this.name);
};
// 測試
var folder = new Folder('學習資料');
var folder1 = new Folder('JavaScript');
folder1.add(new File('JavaScript設計模式與開發實踐'));
folder.add(folder1);
folder.add(new File('深入淺出Node.js'));
console.log('第一次掃描');
folder.scan();
folder1.remove();
console.log('第二次掃描');
folder.scan();
模板方法模式
模板方法模式是一種只需使用繼承就可以實現的非常簡單的模式。
模板方法模式由兩部分結構組成,第一部分是抽象父類,第二部分是具體的實現子類。
假如我們有一些平行的子類,各個子類之間有一些相同的行為,也有一些不同的行為。如果 相同和不同的行為都混合在各個子類的實現中,說明這些相同的行為會在各個子類中重複出現。 但實際上,相同的行為可以被搬移到另外一個單一的地方,模板方法模式就是為解決這個問題而 生的。在模板方法模式中,子類實現中的相同部分被上移到父類中,而將不同的部分留待子類來 實現。這也很好地體現了泛化的思想。
例子:咖啡與茶
先泡一杯咖啡
- 把水煮沸
- 用沸水沖泡咖啡
- 把咖啡倒進杯子
- 加糖和牛奶
var Coffee = function () { };
Coffee.prototype.boilWater = function () {
console.log('把水煮沸');
};
Coffee.prototype.brewCoffeeGriends = function () {
console.log('用沸水沖泡咖啡');
};
Coffee.prototype.pourInCup = function () {
console.log('把咖啡倒進杯子');
};
Coffee.prototype.addSugarAndMilk = function () {
console.log('加糖和牛奶');
};
Coffee.prototype.init = function () {
this.boilWater();
this.brewCoffeeGriends();
this.pourInCup();
this.addSugarAndMilk();
};
var coffee = new Coffee();
coffee.init();
泡一壺茶
- 把水煮沸
- 用沸水浸泡茶葉
- 把茶水倒進杯子
- 加檸檬
var Tea = function () { };
Tea.prototype.boilWater = function () {
console.log('把水煮沸');
};
Tea.prototype.steepTeaBag = function () {
console.log('用沸水浸泡茶葉');
};
Tea.prototype.pourInCup = function () {
console.log('把茶水倒進杯子');
};
Tea.prototype.addLemon = function () {
console.log('加檸檬');
};
Tea.prototype.init = function () {
this.boilWater();
this.steepTeaBag();
this.pourInCup();
this.addLemon();
};
var tea = new Tea();
tea.init();
分離出共同點
- 把水煮沸
- 用沸水沖泡飲料
- 把飲料倒進杯子
- 加調料
var Beverage = function () { };
Beverage.prototype.boilWater = function () {
console.log('把水煮沸');
};
Beverage.prototype.brew = function () { }; // 空方法,應該由子類重寫
Beverage.prototype.pourInCup = function () { }; // 空方法,應該由子類重寫
Beverage.prototype.addCondiments = function () { }; // 空方法,應該由子類重寫
Beverage.prototype.init = function () {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
建立Coffee子類
var Coffee = function () { };
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function () {
console.log('用沸水沖泡咖啡');
};
Coffee.prototype.pourInCup = function () {
console.log('把咖啡倒進杯子');
};
Coffee.prototype.addCondiments = function () {
console.log('加糖和牛奶');
};
var coffee = new Coffee();
coffee.init();
建立Tea子類
var Tea = function () { };
Tea.prototype = new Beverage();
Tea.prototype.brew = function () {
console.log('用沸水浸泡茶葉');
};
Tea.prototype.pourInCup = function () {
console.log('把茶倒進杯子');
};
Tea.prototype.addCondiments = function () {
console.log('加檸檬');
};
var tea = new Tea();
tea.init();
Beverage.prototype.init 被稱為模板方法的原因是,該方法中封裝了子類的演算法框架,它作 為一個演算法的模板,指導子類以何種順序去執行哪些方法。在 Beverage.prototype.init 方法中, 演算法內的每一個步驟都清楚地展示在我們眼前。
享元模式
享元模式是一種用於效能優化的模式,核心是運用共享技術來有效支援大量細粒度的物件。
內部狀態 儲存於共享物件內部,而 外部狀態 儲存於共享物件的外部,在必要時被傳入共享物件來組裝成一個完整的物件。
上傳檔案的例子
下面程式碼同時選擇 2000 個檔案時,會 new 2000 個 upload 物件。
var id = 0;
window.startUpload = function (uploadType, files) { // uploadType 區分是控制元件還是 flash
for (var i = 0; i < files.length; i++) {
var file = files[i];
var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
uploadObj.init(id++);
}
};
var Upload = function (uploadType, fileName, fileSize) {
this.uploadType = uploadType;
this.fileName = fileName;
this.fileSize = fileSize;
this.dom = null;
};
Upload.prototype.init = function (id) {
var that = this;
this.id = id;
this.dom = document.createElement('div');
this.dom.innerHTML = '<span>檔名稱:' + this.fileName + ',檔案大小:' + this.fileSize + '</span>' + '<button class="delFile">刪除</button>';
this.dom.querySelector('.delFile').onclick = function () {
that.delFile();
}
document.body.appendChild(this.dom);
};
Upload.prototype.delFile = function () {
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}
if (window.confirm('確定刪除該檔案嗎?' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
};
// 上傳
startUpload('plugin', [
{ fileName: '1.txt', fileSize: 1000 },
{ fileName: '2.txt', fileSize: 3000 },
{ fileName: '3.txt', fileSize: 5000 },
]);
startUpload('flash', [
{ fileName: '4.txt', fileSize: 1000 },
{ fileName: '5.txt', fileSize: 3000 },
{ fileName: '6.txt', fileSize: 5000 },
]);
享元模式重構檔案上傳
var Upload = function (uploadType) {
this.uploadType = uploadType;
};
Upload.prototype.delFile = function (id) {
uploadManager.setExternalState(id, this);
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}
if (window.confirm('確定刪除該檔案嗎?' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
};
var UploadFactory = (function () {
var createdFlyWeightObjs = {};
return {
create: function (uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType];
}
return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
}
}
})();
var uploadManager = (function () {
var uploadDatabase = {};
return {
add: function (id, uploadType, fileName, fileSize) {
var flyWeightObj = UploadFactory.create(uploadType);
var dom = document.createElement('div');
dom.innerHTML = '<span>檔名稱:' + fileName + ',檔案大小:' + fileSize + '</span>' + '<button class="delFile">刪除</button>';
dom.querySelector('.delFile').onclick = function () {
flyWeightObj.delFile(id);
};
document.body.appendChild(dom);
uploadDatabase[id] = {
fileName: fileName,
fileSize: fileSize,
dom: dom,
};
return flyWeightObj;
},
setExternalState: function (id, flyWeightObj) {
var uploadData = uploadDatabase[id];
for (var key in uploadData) {
flyWeightObj[key] = uploadData[key];
}
}
}
})();
var id = 0;
window.startUpload = function (uploadType, files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
var uploadObj = uploadManager.add(++this.id, uploadType, file.fileName, file.fileSize);
}
};
// 上傳
startUpload('plugin', [
{ fileName: '1.txt', fileSize: 1000 },
{ fileName: '2.txt', fileSize: 3000 },
{ fileName: '3.txt', fileSize: 5000 },
]);
startUpload('flash', [
{ fileName: '4.txt', fileSize: 1000 },
{ fileName: '5.txt', fileSize: 3000 },
{ fileName: '6.txt', fileSize: 5000 },
]);
享元模式重構後,無論上傳多少次,Upload 物件(內部狀態)的數量一直是 2。
職責鏈模式
使多個物件都有機會處理請求,從而避免請求的傳送者和接收者之間的耦合關係,將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理它為止。
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); // 輸出:手機庫存不足
用 AOP 實現指責鏈
Function.prototype.after = function (fn) {
var self = this;
return function () {
var ret = self.apply(this, arguments);
if (ret === 'nextSuccessor') {
return fn.apply(this, arguments);
}
return ret;
}
};
var order = order500.after(order200).after(orderNormal);
order(1, true, 500); // 輸出:500 元定金預購,得到 100 優惠券
order(2, true, 500); // 輸出:200 元定金預購,得到 50 優惠券
order(1, false, 500); // 輸出:普通購買,無優惠券
中介者模式
中介者模式的作用就是解除物件與物件之間的緊耦合關係。增加一箇中介者物件後,所有的 相關物件都通過中介者物件來通訊,而不是互相引用,所以當一個物件發生改變時,只需要通知 中介者物件即可。
變成了
裝飾者模式
為物件動態加入行為。裝飾者模式經常會形成一條長長的裝飾鏈。
物件導向裝飾者模式
// 原始的飛機類
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();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈
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();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈
狀態模式
狀態模式的關鍵是區分事物內部的狀態,事物內部狀態的改變往往會帶來事物的行為改變。
例子:電燈的 弱光、強光、關燈 切換。
// OffLightState:
var OffLightState = function (light) {
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
console.log('弱光'); // offLightState 對應的行為
this.light.setState(this.light.weakLightState); // 切換狀態到 weakLightState
};
// WeakLightState:
var WeakLightState = function (light) {
this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
console.log('強光'); // weakLightState 對應的行為
this.light.setState(this.light.strongLightState); // 切換狀態到 strongLightState
};
// StrongLightState:
var StrongLightState = function (light) {
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
console.log('關燈'); // strongLightState 對應的行為
this.light.setState(this.light.offLightState); // 切換狀態到 offLightState
};
var Light = function () {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
Light.prototype.init = function () {
var button = document.createElement('button'),
self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '開關';
this.currState = this.offLightState; // 設定當前狀態
this.button.onclick = function () {
self.currState.buttonWasPressed();
}
};
Light.prototype.setState = function (newState) {
this.currState = newState;
};
var light = new Light();
light.init();
介面卡模式
介面卡模式的作用是解決兩個軟體實體間的介面不相容的問題。
var googleMap = {
show: function () {
console.log('開始渲染谷歌地圖');
}
};
var baiduMap = {
display: function () {
console.log('開始渲染百度地圖');
}
};
// 新增百度地圖介面卡
var baiduMapAdapter = {
show: function () {
return baiduMap.display();
}
};
renderMap(googleMap); // 輸出:開始渲染谷歌地圖
renderMap(baiduMapAdapter); // 輸出:開始渲染百度地圖
總結
- 單例模式:保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。
- 策略模式:封裝一系列目標一致的‘業務規則’。
- 代理模式:當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。
- 迭代器模式:迭代器模式是指提供一種方法順序訪問一個聚合物件中的各個元素,而又不需要暴露該物件的內部表示。
- 釋出-訂閱模式:定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。
- 命令模式:程式碼封裝成命令,目的解藕。
- 組合模式:組合物件-葉物件結構。
- 模板方法模式:抽象父類,具體的實現子類。
- 享元模式:一種用於效能優化的模式,核心是運用共享技術來有效支援大量細粒度的物件。
- 職責鏈模式:使多個物件都有機會處理請求,當前不能解決則拋給下一個。
- 中介者模式:解除物件與物件之間的緊耦合關係,使多對多變成了一對多。
- 裝飾者模式:為物件動態加入行為。裝飾者模式經常會形成一條長長的裝飾鏈。
- 狀態模式:狀態改變,行為改變。
- 介面卡模式:解決兩個軟體實體間的介面不相容的問題。