本文源于本人關(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沒有繼承機(jī)制,但是子類如果繼承了父類沒有重寫方法,編輯器不會提醒,那么執(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é)任鏈模式
定義:使多個對象都有機(jī)會處理請求,從而避免請求的發(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('手機(jī)庫存不足');
}
}
編寫責(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)系。
例子:
- 機(jī)場指揮中心:每架飛機(jī)不可能和其他所有飛機(jī)逐一聯(lián)系,來確定是否能起飛,是否能滑動,這樣的聯(lián)系都交給了指揮中心來做。每架飛機(jī)只需要聯(lián)系中介者即可。
- 博彩公司算賠率:和機(jī)場指揮中心是一樣的道理。
前端的利用:
商品購買:通常商品購買會有選擇框,輸入框,還有信息提示框,我們需要選擇或者輸入時,信息都能有正確的提示,一個辦法是強(qiáng)耦合,在選擇框變動后,去修改提示框。如果添加新的選擇框,代碼變動會更大。
引入中介者:具體處理邏輯交給中介者處理,其他選擇框只與中介者交互。
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">請選擇手機(jī)顏色和購買數(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 = '請選擇手機(jī)顏色';
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ù)雜一點呢:新添加一個按鈕,如果這個按鈕按下,那么點燈是弱-強(qiáng)-關(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)變等。