本文源于本人關(guān)于《JavaScript設(shè)計模式與開發(fā)實踐》(曾探著)的閱讀總結(jié)媚赖。想詳細(xì)了解具體內(nèi)容建議閱讀該書。
- /# :表示重點設(shè)計模式
- 原文代碼:js-design-mode
1. 策略模式:
定義:定義一系列的算法,把他們一個個封裝起來蜗巧,并且使他們可以相互替換。
前端中的利用:表單驗證(不同的表單有不同的驗證方式)
一個簡單的例子:公司發(fā)獎金根據(jù)每個人的績效不同來發(fā)不同的獎金,不同的績效焊夸,獎金有不同的計算方式。 我們可以用if-else蓝角,判斷每個人的績效是什么阱穗,從而采用不同的計算方式。但是如果又增加了一個種績效水平使鹅,那么我們又得增加if-else分支揪阶,這明顯是違反開放-封閉原則的。
核心思想:創(chuàng)建一個策略組患朱,每次有新的績效計算方法則直接加入該組里遣钳,不會變動其他代碼。 調(diào)用時麦乞,傳入績效字符串蕴茴,從而采用調(diào)用屬性的方法訪問到正確策略,并調(diào)用該策略姐直。
利用策略模式構(gòu)建獎金發(fā)放:
var strategies = {
"S": function(salary) {
return salary * 4;
},
"A": function(salary) {
return salary * 3;
},
"B": function(salary) {
return salary * 2;
}
}
function calculateBonus(level, salary) {
return strategies[level](salary);
}
console.log(calculateBonus('A', 13333));
2. 代理模式:
定義:提供一個代用品或占位符倦淀,以便控制對它的訪問。
前端中的利用:圖片預(yù)加載(loading圖片)声畏、緩存代理
核心思想:對象A訪問對象B撞叽,創(chuàng)建一個對象C,控制對象A對對象B的訪問插龄,從而達(dá)到某種目的愿棋。 或者A進(jìn)行某個行為,創(chuàng)建一個對象C控制A進(jìn)行的這個行為均牢。
圖片預(yù)加載:
var myImage = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
}
}
})()
它返回了一個對象糠雨,擁有普通的圖片加載功能,但是這個功能有一個弊端徘跪,網(wǎng)絡(luò)環(huán)境差甘邀,圖片遲遲沒有完全加載完成時琅攘,會產(chǎn)生一個白框,我們希望這個時候有一個loading的動畫松邪。
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage.setSrc(this.src);
}
return {
setSrc: function (src) {
myImage.setSrc('./屏幕快照 2017-09-19 上午10.15.58.png');
img.src = src;
}
}
})()
現(xiàn)在創(chuàng)建了一個代理坞琴,我們想要加載圖片時,并不直接調(diào)用圖片加載對象逗抑,而是調(diào)用這個代理函數(shù)剧辐,達(dá)到有l(wèi)oading動畫的目的。
它先把imgNode設(shè)置為loading動畫的gif圖片邮府,然后創(chuàng)建了一個Image對象荧关,等傳入的真實圖片鏈接,圖片加載完成后挟纱,再用真實圖片替換掉loading動畫gif羞酗。
當(dāng)你已經(jīng)寫完了某個函數(shù),但是某時希望這個函數(shù)的行為有其他效果時紊服,你就可以寫一個代理達(dá)到你的目的檀轨。
3. 迭代器模式:
定義:提供一種方法順序訪問一個聚合對象中的各個元素。
前端中的利用:循環(huán)
很多語言都內(nèi)置了迭代器欺嗤,我們很多時候不認(rèn)為他是一種設(shè)計模式参萄。
這里我們說一下外部迭代器:
- 必須顯式地請求迭代下一個元素。
- 增加了一些調(diào)用的復(fù)雜性煎饼,但是更為靈活讹挎,我們可以手工控制迭代過程和順序。
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 compare = function(iterator1, iterator2) {
if(iterator1.length!==iterator2.length) {
console.log('不相等');
}
while(!iterator1.isDone() && !iterator2.isDone()){
if(iterator1.getCurrItem() !== iterator2.getCurrItem()){
console.log('不相等');
}
iterator1.next();
iterator2.next();
}
console.log('相等');
}
compare(Iterator([1, 2, 3]), Iterator([1, 2, 3])); // 相等
4. 命令模式
定義:指的是一個執(zhí)行某些特定事情的指令吆玖。
使用場景:有時候需要向某些對象發(fā)送請求筒溃,但是并不知道請求的接收者是誰,也不知道被請求的操作是什么沾乘。
前端中的利用:菜單程序怜奖,按鍵動畫
背景:前端協(xié)作中,有人負(fù)責(zé)寫界面翅阵,有人負(fù)責(zé)開發(fā)按鈕之類的具體功能歪玲。我們希望寫界面的人直接調(diào)用命令就好,不用關(guān)心掷匠,具體實現(xiàn)滥崩。
按鍵動畫(每個按鍵代表不同的動畫):
命令創(chuàng)建函數(shù):
var makeCommand = function (receiver, state) {
return function () {
receiver[state]();
}
}
receiver代表具體動畫的執(zhí)行函數(shù)。
界面同學(xué)只負(fù)責(zé):
document.onkeypress = function (ev) {
var keyCode = ev.keyCode,
command = makeCommand(Ryu, commands[keyCode]);
if (command) {
command();
}
};
而實現(xiàn)操作的同學(xué)寫具體實現(xiàn)讹语,和不同按鍵所對應(yīng)的指令名稱:
var Ryu = {
attack: function () {
console.log('攻擊');
},
defense: function () {
console.log('防御');
},
jump: function () {
console.log('跳躍');
},
crouch: function () {
console.log('下蹲');
}
};
var commands = {
'119': 'jump', // W
'115': 'crouch', // S
'97': 'defense', // A
'100': 'attack' // D
}
目前我們的命令模式钙皮,只有一個設(shè)置命令,但是這其實完全可以寫成一個對象,包含株灸,記錄命令調(diào)用過程崇摄,包含取消命令擎值,等等慌烧。
5. 組合模式:
定義:將對象組合成樹形結(jié)果,以表示“部分-整體”的層次結(jié)果鸠儿。除了用來表示樹形結(jié)構(gòu)之外屹蚊,組合模式令一個好處是通過對象的多態(tài)性表現(xiàn),使得用戶對單個對象和組合對象的使用具有一致性进每。
前端中的利用:文件夾掃描
核心思想:樹形結(jié)構(gòu)汹粤,分為葉子對象和非葉子對象, 葉子對象和非葉子對象擁有一組同樣的方法屬性田晚, 調(diào)用非葉子對象的方法后嘱兼,該對象和該對象下的所有對象都會執(zhí)行該方法。
文件掃描:當(dāng)我們負(fù)責(zé)粘貼時贤徒,我們不會關(guān)心我們選中的是文件還是文件夾芹壕,我們都會一并進(jìn)行負(fù)責(zé)粘貼。
文件夾:
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; file = this.files[i++];) {
file.scan();
}
}
文件:
var File = function(name){
this.name = name;
}
File.prototype.add = function() {
throw new Error('文件下面不能再添加文件');
}
File.prototype.scan = function() {
console.log('開始掃描文件:' + this.name);
}
組成文件結(jié)構(gòu):
var folder = new Folder('學(xué)習(xí)資料');
var folder1 = new Folder('JS');
var folder2 = new Folder('JQ');
var file = new File('學(xué)習(xí)資料');
var file1 = new File('學(xué)習(xí)資料1');
var file2 = new File('學(xué)習(xí)資料2');
var file3 = new File('學(xué)習(xí)資料3');
folder.add(file);
folder.add(file1);
folder1.add(file2);
folder2.add(file3);
var rootFolder = new Folder('root');
rootFolder.add(folder);
rootFolder.add(folder1);
rootFolder.add(folder2);
掃描:
rootFolder.scan();
// 輸出:
// 開始掃描文件夾:root
// 開始掃描文件夾:學(xué)習(xí)資料
// 開始掃描文件:學(xué)習(xí)資料
// 開始掃描文件:學(xué)習(xí)資料1
// 開始掃描文件夾:JS
// 開始掃描文件:學(xué)習(xí)資料2
// 開始掃描文件夾:JQ
// 開始掃描文件:學(xué)習(xí)資料3
6. 模版方法模式
定義:由兩部分結(jié)構(gòu)組成接奈,第一部分就是抽象父類踢涌,第二部分就是具體實現(xiàn)的子類。通常父類中封裝了子類的算法框架序宦,包括實現(xiàn)一些公共方法及封裝子類中所有方法的執(zhí)行順序睁壁。
使用場景:假如我們有一些平行的子類,各個子類之間有一些相同的行為互捌,也有一些不同的行為潘明。如果相同和不同的行為都混合在各個子類的實現(xiàn)中,說明這些相同的行為會在各個子類中重復(fù)出現(xiàn)秕噪。
模版方法模式所做的事情:我們不必重寫一個子類钳降,如果屬于同一類型就可以直接繼承抽象類,然后把變化的邏輯封裝到子類中即可巢价,不需要改動其他子類和父類牲阁。
例子:
- 泡咖啡:
- 把水煮沸
- 把沸水沖泡咖啡
- 把咖啡倒進(jìn)杯子
- 加糖和牛奶
- 泡茶:
- 把水煮沸
- 用沸水浸泡茶葉
- 把水倒進(jìn)杯子里
- 加檸檬
然后進(jìn)行抽象:
- 把水煮沸
- 用沸水沖泡飲料
- 把飲料倒進(jìn)杯子里
- 加調(diào)料
抽象類代碼:
var Beverage = function() {};
Beverage.prototype.boilWater = function(){
console.log('把水煮沸');
};
// 空方法,應(yīng)該由子類來重寫
Beverage.prototype.brew = function() {
throw new Error('子類必須重寫brew方法');
};
// 空方法壤躲,應(yīng)該由子類來重寫
Beverage.prototype.pourInCup = function() {
throw new Error('子類必須重寫pourInCup方法');
};
// 空方法城菊,應(yīng)該由子類來重寫
Beverage.prototype.addCondiments = function() {
throw new Error('子類必須重寫addCondiments方法');
};
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
因為JS沒有繼承機制,但是子類如果繼承了父類沒有重寫方法碉克,編輯器不會提醒凌唬,那么執(zhí)行的時候會報錯,為了防止程序員漏重寫方法漏麦,故在需要重寫的方法中拋出異常客税。
coffee:
var Coffee = function() {};
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function() {
console.log('用水沖泡咖啡');
};
Coffee.prototype.pourInCup = function() {
console.log('把咖啡倒進(jìn)杯子里');
};
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('把茶水倒進(jìn)杯子里');
};
Tea.prototype.addCondiments = function() {
console.log('加檸檬');
};
var tea = new Tea();
tea.init();
# 7. 單例模式
定義:保證一個類僅有一個實例况褪,并提供一個訪問它的全局訪問點。
前端中的利用:登錄框更耻,彈層
核心思想:利用一個變量保存第一次創(chuàng)建的結(jié)果(對象中的某個屬性或者閉包能訪問的變量)测垛, 再次創(chuàng)建時,該變量不為空秧均,直接返回改對象食侮。
類:
var Singleton = function(name) {
this.name = name;
this.instance = null;
}
Singleton.prototype.getName = function() {
console.log(this.name);
}
Singleton.getInstance = function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
var a = Singleton.getInstance('123');
var b = Singleton.getInstance('321');
console.log(a === b); // true
Singleton.getInstance是靜態(tài)方法。
通用的惰性單例:
function getSingleton(fn) {
var instance = null;
return function() {
return instance || (instance = fn.apply(this, arguments) );
}
}
var createObj = function(name) {
return {name: name};
}
var getSingleObj = getSingleton(createObj);
console.log(getSingleObj('123') === getSingleObj('321'));
fn為實例創(chuàng)建函數(shù)目胡,用通用的單例模式包裝之后锯七,他就變成了單例創(chuàng)建函數(shù)。
# 8. 發(fā)布-訂閱模式
定義:也可以叫觀察者模式誉己,它定義對象間的一種一對多的依賴關(guān)系眉尸,當(dāng)一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知巨双。
前端中的利用:Vue雙向綁定噪猾、事件監(jiān)聽函數(shù)。
一個例子-售樓處:
- 很多人登記了信息炉峰,當(dāng)有樓盤的時候畏妖,將會通知所有人前來購買。
- 但是每個人的經(jīng)濟(jì)能力有限疼阔,有些人關(guān)注的是別墅樓盤戒劫,有些人關(guān)注的是小戶樓盤,所以每個行為訂閱的內(nèi)容也不一樣婆廊。
- 有些人嫌這家售樓處的服務(wù)態(tài)度不好迅细,想取消訂閱。
通用實現(xiàn):創(chuàng)建一個訂閱-發(fā)布對象淘邻,該對象擁有一個客戶組對象茵典,擁有訂閱方法,發(fā)布方法宾舅,取消方法统阿。
- 當(dāng)訂閱時:將客戶訂閱的內(nèi)容,和執(zhí)行方法存在客戶組對象中:
listen = function (key, fn) {
if (!cacheList[key]) {
cacheList[key] = [];
}
cacheList[key].push(fn);
};
- 取消訂閱時:
remove = function (key, fn) {
var fns = cacheList[key];
if (!fns) return false;
// 如果只傳了key 代表取消該key下所有客戶
if (!fn) {
fns && (fns.length = 0);
} else {
for (var i = fns.length - 1; i >= 0; i--) {
if (fns[i] === fn) {
fns.splice(i, 1);
}
}
}
};
- 發(fā)布:
trigger = function () {
var key = Array.prototype.shift.call(arguments),
args = arguments,
fns = cacheList[key];
if (!fns || fns.length === 0) return false;
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, args);
}
}
其實僅僅只有一個客戶組時遠(yuǎn)遠(yuǎn)不夠的筹我,更應(yīng)該有創(chuàng)建命名空間的功能扶平,詳見《JavaScript設(shè)計模式與實踐》8.11。
# 9. 享元模式
定義:享元模式是一種用于性能優(yōu)化的模式蔬蕊,核心運用共享技術(shù)來支持大量細(xì)粒度的對象结澄。
例子:我們有50件男士內(nèi)衣,和50件女士內(nèi)衣,我們需要模特穿上拍照麻献。 我們有兩種可能性:
- 為50件男士內(nèi)衣找50個男模特分別拍照 们妥,為50件女士內(nèi)衣找50個女模特分別拍照。
- 找一個男模特勉吻,和一個女模特监婶,分別穿50次照相。(享元模式)
這個便是享元模式的模型餐曼,目的在于減少共享對象的數(shù)量压储,我們需要將對象分為內(nèi)部狀態(tài)和外部狀態(tài):
- 內(nèi)部狀態(tài)存在于對象內(nèi)部
- 內(nèi)部狀態(tài)可以共享
- 內(nèi)部狀態(tài)獨立與場景鲜漩,通常不會改變源譬。
- 外部狀態(tài)決定于場景,根據(jù)場景的變化而改變孕似。
上面的例子中踩娘,性別是內(nèi)部狀態(tài),內(nèi)衣是外部狀態(tài)喉祭,通過區(qū)分這兩種狀態(tài)來減少系統(tǒng)的對象數(shù)量殉簸。
前端中的利用:
文件上傳:用戶選中文件之后邢滑,掃碼文件后,為每個文件創(chuàng)建一個upload對象,每個upload對象有一個上傳類型(插件上傳吝梅,F(xiàn)lash上傳等,不同文件可能適合不同的上傳方式)玉雾,但是如果用戶一次性選擇的文件太多跨算,則會出現(xiàn)對象過多,對象爆炸鹉究。
我們利用以上的方法宇立,分離出外部狀態(tài)和內(nèi)部狀態(tài)。 每個共享對象不變的應(yīng)該是它的上傳類型(內(nèi)部狀態(tài))自赔,而改變的是每個上傳對象的此時此刻擁有的文件妈嘹,不同的文件就是外部狀態(tài)。
創(chuàng)建upload對象:
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);
}
}
每次要刪除文件的時候润脸,將這個共享對象指向觸發(fā)點擊函數(shù)的文件,執(zhí)行刪除該文件他去,對象仍然保留毙驯。
創(chuàng)建不同內(nèi)部狀態(tài)的對象(被共享的不同上傳類型的對象):
var UploadFactory = (function () {
var createdFlyWeightObjs = {};
return {
create: function (uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType];
}
return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
}
}
})()
定義了一個工廠模式來創(chuàng)建upload對象,如果某種內(nèi)部狀態(tài)對應(yīng)的共享狀態(tài)已經(jīng)被創(chuàng)建過孤页,那么直接返回這個對象尔苦,否則創(chuàng)建一個新的對象。
管理器封裝外部狀態(tài):
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, fileSize, dom
};
return flyWeightObj;
},
setExternalState: function (id, flyWeightObj) {
var uploadData = uploadDatabase[id];
for (var i in uploadData) {
flyWeightObj[i] = uploadData[i];
}
}
}
})()
uploadManager對象負(fù)責(zé)像UploadFactory提交創(chuàng)建對象的請求魂那,并用一個uploadDatabase對象保存upload對象的所有外部狀態(tài)。
上傳函數(shù):
var id = 0;
window.startUpload = function (uploadType, files) {
for (var i = 0, file; file = files[i++];) {
var uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
}
}
startUpload('plugin', [
{
fileName: '1.txt',
fileSize: 1000,
},
{
fileName: '2.txt',
fileSize: 2000,
},
{
fileName: '3.txt',
fileSize: 3000,
}
]);
startUpload('Flash', [
{
fileName: '4.txt',
fileSize: 4000,
},
{
fileName: '5.txt',
fileSize: 5000,
},
{
fileName: '6.txt',
fileSize: 6000,
}
]);
現(xiàn)在不管上傳6個文件稠项,還是2000個文件涯雅,都只會創(chuàng)建2個對象。
核心思想:
- 創(chuàng)建能共享的對象展运,每個不同的能共享的對象區(qū)別在于內(nèi)部狀態(tài)的不同(uploadType)活逆。
- 每個共享的對象依然加上自己的操作,但是在執(zhí)行操作之前拗胜,需要將共享對象指向當(dāng)前外部狀態(tài)(文件)蔗候。
- 創(chuàng)建一個工廠,能夠創(chuàng)建不同內(nèi)部狀態(tài)都共享對象埂软,如果該種內(nèi)部狀態(tài)的共享對象已經(jīng)存在锈遥,則直接返回。
- 創(chuàng)建一個外部狀態(tài)管理對象勘畔,包含一個數(shù)據(jù)庫對象存儲不同外部狀態(tài)所灸,包含一個添加函數(shù),和指向函數(shù)(共享對象指向外部狀態(tài))炫七。
# 10. 責(zé)任鏈模式
定義:使多個對象都有機會處理請求爬立,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對象連成一條鏈万哪,并沿著該鏈傳遞該請求侠驯,直到有一個對象處理它為止。
例子:高峰期公交車壤圃,我們不能直接把錢遞給售票員陵霉,直接給離得比較近的一個人,一直傳遞下去伍绳,最終會到售票員手里踊挠。
前端中的利用:
電商網(wǎng)站不同用戶種類的下單策略:
- orderType1用戶:已經(jīng)支付500元,得到100元優(yōu)惠券冲杀;未支付500效床,降級到普通用戶購買界面。
- orderType2用戶:已經(jīng)支付200元权谁,得到50元優(yōu)惠券剩檀;未支付200,降級到普通用戶購買界面旺芽。
- orderType3用戶:普通購買沪猴。
- 庫存限制辐啄,針對code3。
新手寫法:根據(jù)orderType运嗜,isPay壶辜,stock來寫if-else分支來進(jìn)行具體操作。
責(zé)任鏈模式寫法:
分別寫order500担租、order200砸民、orderNormal的函數(shù),如果滿足條件則執(zhí)行奋救,不滿足條件則返回一個字段表示交給下一個節(jié)點執(zhí)行:
var order500 = function(orderType, pay, stock) {
if(orderType === 1 && pay === true) {
console.log('500元訂金預(yù)購岭参,得到100優(yōu)惠券');
} else {
return 'nextSuccessor';
}
};
var order200 = function(orderType, pay, stock) {
if(orderType === 2 && pay === true) {
console.log('200元訂金預(yù)購,得到50優(yōu)惠券');
} else {
return 'nextSuccessor';
}
};
var orderNormal = function(orderType, pay, stock) {
if(stock > 0) {
console.log('普通購買尝艘,無優(yōu)惠券');
} else {
console.log('手機庫存不足');
}
}
編寫責(zé)任鏈控制函數(shù):
var Chain = function(fn) {
this.fn = fn;
this.successor = null;
}
Chain.prototype.setNextSuccessor = function(successor){
return this.successor = successor;
}
Chain.prototype.passRequest = function(){
// 執(zhí)行該節(jié)點的具體方法
var ret = this.fn.apply(this, arguments);
// 如果執(zhí)行結(jié)果未不滿足演侯,則調(diào)用下一個節(jié)點的執(zhí)行方法
if(ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
};
類似于鏈表,每個節(jié)點都保存著下一個節(jié)點利耍,并含有一個該節(jié)點的執(zhí)行函數(shù)蚌本,和設(shè)置下一個節(jié)點的函數(shù)。
// 將每個具體執(zhí)行函數(shù)封裝為一個責(zé)任鏈節(jié)點
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
// 設(shè)置每個節(jié)點的下一個節(jié)點
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
chainOrder500.passRequest(1, true, 500);
chainOrder500.passRequest(2, true, 500);
chainOrder500.passRequest(3, true, 500);
chainOrder500.passRequest(1, false, 0);
這樣只需要第一個節(jié)點執(zhí)行隘梨,如果不滿足則請求自動交付給下一個節(jié)點,直到到達(dá)節(jié)點尾部舷嗡。
如果未來還有更多情況轴猎,比如有交了50定金的,可以給10元的優(yōu)惠券进萄,這樣的情況可以直接添加節(jié)點捻脖,改變節(jié)點順序,不會對已有的方法做更改中鼠。
本例子可以用Promise做該寫可婶,如果成功則Promise.resolve()否則Promise.reject()
還可以使用AOP的方式Function.prototype.after做改寫。
核心思想:將具體執(zhí)行方法包裝為一個個責(zé)任鏈子節(jié)點援雇,執(zhí)行第一個節(jié)點矛渴,如果情況滿足則執(zhí)行,不滿足則調(diào)用下一個節(jié)點的執(zhí)行方法惫搏。
# 11. 中介者模式
定義:將行為分布到各個對象中具温,把對象劃分為更小的細(xì)粒度,但是由于細(xì)粒度之間對象的聯(lián)系激增筐赔,又有可能反過來降低它們的可復(fù)用性铣猩。中介者模式使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系。
例子:
- 機場指揮中心:每架飛機不可能和其他所有飛機逐一聯(lián)系茴丰,來確定是否能起飛达皿,是否能滑動天吓,這樣的聯(lián)系都交給了指揮中心來做。每架飛機只需要聯(lián)系中介者即可峦椰。
- 博彩公司算賠率:和機場指揮中心是一樣的道理失仁。
前端的利用:
商品購買:通常商品購買會有選擇框,輸入框们何,還有信息提示框萄焦,我們需要選擇或者輸入時,信息都能有正確的提示冤竹,一個辦法是強耦合拂封,在選擇框變動后,去修改提示框鹦蠕。如果添加新的選擇框冒签,代碼變動會更大。
引入中介者:具體處理邏輯交給中介者處理钟病,其他選擇框只與中介者交互萧恕。
html:
<body>
選擇顏色:
<select name="" id="colorSelect">
<option value="">請選擇</option>
<option value="red">紅色</option>
<option value="blue">藍(lán)色</option>
</select>
<br> 選擇內(nèi)存:
<select name="" id="memorySelect">
<option value="">請選擇</option>
<option value="32G">32g</option>
<option value="16G">16g</option>
</select>
<br>
<br> 輸入購買數(shù)量:
<input type="text" id="numberInput">
<br>
<br> 您選擇了顏色:
<div id="colorInfo"></div>
<br> 您選擇了內(nèi)存:
<div id="memoryInfo"></div>
<br> 您輸入了數(shù)量:
<div id="numberInfo"></div>
<br>
<button id="nextBtn" disabled="true">請選擇手機顏色和購買數(shù)量</button>
</body>
獲取各種框dom節(jié)點:
var colorSelect = document.getElementById('colorSelect');
var memorySelect = document.getElementById('memorySelect');
var numberInput = document.getElementById('numberInput');
var colorInfo = document.getElementById('colorInfo');
var memoryInfo = document.getElementById('memoryInfo');
var numberInfo = document.getElementById('numberInfo');
var nextBtn = document.getElementById('nextBtn');
編寫中介者:
var mediator = (function () {
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 = '請選擇內(nèi)存大小';
return;
}
if (!(Number.isInteger(number - 0) && number > 0)) {
nextBtn.disabled = true;
nextBtn.innerHTML = '請輸入正確的購買數(shù)量';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入購物車';
}
}
})();
變動只與中介者交互:
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numberInput.oninput = function() {
mediator.changed(this);
};
12. 裝飾者模式
定義:在不改變對象自身的基礎(chǔ)上,在程序運行期間給對象動態(tài)添加職責(zé)肠阱。(包裝器)
例子:
- 給自行車擴(kuò)展票唆,給4種自行車擴(kuò)展3個配件,在繼承的基礎(chǔ)上需要建立出12個子類屹徘。
- 但是動態(tài)的把這些動態(tài)添加到自行車上則住需要額外3個類(3個配件)走趋。
裝飾者:
// 保存引用的裝飾者模式
var plane = {
fire: function() {
console.log('發(fā)射普通子彈');
}
}
var missileDecorator = function() {
console.log('發(fā)射導(dǎo)彈');
}
var atomDecorator = function() {
console.log('發(fā)射原子彈');
}
var fire1 = plane.fire;
plane.fire = function() {
fire1();
missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function() {
fire2();
atomDecorator();
}
plane.fire();
AtomDecorator 包裝 MissileDecorator 包裝 Plane。 這樣寫完全符合開發(fā)-封閉原則噪伊,在添加新功能的時候沒有去改動別人點方法簿煌,但是不好的就是,如果包裝點層次太多鉴吹,中間變量就太多了姨伟。還會遇見this劫持的問題:
var _getEleById = document.getElementById;
document.getElementById = function(id) {
alert(1);
return _getElementById(id);
}
this被劫持了。
解決以上兩個問題的最好方法就上AOP函數(shù):
Function.prototype.before = function (fn) {
var _self = this; // 保存原函數(shù)的引用
return function () { // 返回了包含原函數(shù)和新函數(shù)的代理函數(shù)
fn.apply(this, arguments);
return _self.apply(this, arguments); // 執(zhí)行原函數(shù)
}
}
Function.prototype.after = function (fn) {
var _self = this; // 保存原函數(shù)的引用
return function () {
var ret = _self.apply(this, arguments);
fn.apply(this, arguments);
return ret;
}
}
- 第一個:返回在函數(shù)之前執(zhí)行
- 第二個:返回在函數(shù)之后執(zhí)行
前端的利用:數(shù)據(jù)上報這樣和業(yè)務(wù)邏輯無關(guān)的函數(shù)都可以利用包裝者進(jìn)行包裝豆励。
# 13. 狀態(tài)模式:
定義:區(qū)分事物的內(nèi)部狀態(tài)夺荒,事物的內(nèi)部狀態(tài)的改變往往會帶來事物行為的改變。
例子:
- 通常的電燈肆糕,只有一個按鈕般堆,按下按鈕;
- 如果電燈是關(guān)的:那么開燈
- 如果點燈是開著的:那么關(guān)燈
這里換成代碼诚啃,就是簡單的if-else淮摔,但是如果再復(fù)雜一點呢:新添加一個按鈕,如果這個按鈕按下始赎,那么點燈是弱-強-關(guān)模式和橙;否則是開-關(guān)模式仔燕。
這個時候你已經(jīng)開始發(fā)現(xiàn)if-else代碼的缺點了:
- 每次燈擴(kuò)展,都需要修改內(nèi)部代碼魔招,違反開放-封閉原則
- 所有與行為有關(guān)的事情都在一個函數(shù)里
- 狀態(tài)切換不明顯晰搀,僅僅只有一個字段的改變
- if-else太多太繁雜。
狀態(tài)模式下的點燈程序(假設(shè)這里只有一個按鈕办斑,切換開關(guān)):
我們第一步創(chuàng)建點燈(富含狀態(tài)的這個對象):
var Light = function () {
this.currState = FSM.off;
this.button = null;
};
this.currState代表的是不同的狀態(tài):這里的狀態(tài)用對象來表示外恕,開關(guān)兩個狀態(tài)就是兩個對象:
var FSM = {
off: {
buttonWasPressed: function () {
console.log('關(guān)燈');
this.button.innerHTML = '下一次按我是開燈';
this.currState = FSM.on;
}
},
on: {
buttonWasPressed: function() {
console.log('開燈');
this.button.innerHTML = '下一次按我是關(guān)燈';
this.currState = FSM.off;
}
}
}
接下來編寫初始化電燈函數(shù):
Light.prototype.init = function () {
var button = document.createElement('button'),
self = this;
button.innerHTML = '已關(guān)燈';
this.button = document.body.appendChild(button);
this.button.onclick = function () {
self.currState.buttonWasPressed.call(self);
}
};
給按鈕綁定事件,按鈕觸發(fā)時乡翅,觸發(fā)當(dāng)前狀態(tài)對象的更替事件鳞疲。(執(zhí)行外部行為,切換當(dāng)前的狀態(tài))
總結(jié):
狀態(tài)模式編寫思路:
- 設(shè)計富含狀態(tài)的對象(主對象):
- 編寫各種狀態(tài)下的行為
- 狀態(tài)屬性
- 初始化:綁定按鈕蠕蚜,在該狀態(tài)下的狀態(tài)切換
- 設(shè)計各種狀態(tài)對象:
- 接收主對象的this
- 按鈕觸發(fā)時尚洽,改狀態(tài)利用this,多狀態(tài)則編寫多個不同觸發(fā)函數(shù)
切換主對象狀態(tài)靶累、調(diào)用主狀態(tài)行為
與策略模式的區(qū)別:
- 策略模式中每個策略類相互平等沒有關(guān)系
- 狀態(tài)模式中狀態(tài)類之間的關(guān)系是提前確定好的腺毫。
14. 適配器模式
定義:解決兩個軟件實體之間的接口不兼容的問題。
例子:插頭轉(zhuǎn)換器挣柬,轉(zhuǎn)換不同地區(qū)的電壓問題潮酒。
前端中:
- 地圖渲染:
假如地圖渲染的函數(shù)是這樣的:
var renderMap = function( map ) {
if(map.show instanceof Function) {
map.show();
}
}
地圖:
var googleMap = {
show() {
console.log('google地圖開始渲染');
}
}
var baiduMap = {
display() {
console.log('baidu地圖開始渲染');
}
}
我們可以只帶googleMap沒有問題,但是baiduMap提供的接口名明顯不一致凛忿,如果去改renderMap函數(shù)違反了開放封閉原則澈灼。
那么現(xiàn)在我們只能用適配器包裝一下baiduMap:
var baiduMapAdapter = {
show() {
return baiduMap.display();
}
}
思路:封裝與其他不同的方法或者對象,而不要去改動原有的函數(shù)店溢。
其他例子:xml與json格式適配,json與對象格式的轉(zhuǎn)變等委乌。