摘要:代理模式,迭代器模式,單例模式,裝飾者模式最少知識原則一個軟件實體應當盡可能少地與其他實體發生相互作用。迭代器模式可以將迭代的過程從業務邏輯中分離出來,在使用迭代器模式之后,即不用關心對象內部構造也可以按順序訪問其中的每個元素。
接手項目越來越復雜的時候,有時寫完一段代碼,總感覺代碼還有優化的空間,卻不知道從何處去下手。設計模式主要目的是提升代碼可擴展性以及可閱讀性。
本文主要以例子的方式展示設計模式應該如何使用!(例子主要來源于javascript設計模式一書,如果已經對這本書讀得滾瓜爛熟的,可以劃過,如果還未讀,或者想了解一下可以收藏起來慢慢看~)
在使用設計模式前應該需要知道的幾個原則(其中對應設計模式滿足對應原則):
單一職責原則(SRP): 一個對象(只做一件事)。
代理模式,迭代器模式,單例模式,裝飾者模式
最少知識原則(LKP): 一個軟件實體應當盡可能少地與其他實體發生相互作用。
中介者模式
開放-封閉原則(OCP):軟件實體(類,模塊,函數)應該都是可以擴展,但是不可修改
發布-訂閱模式,模板方法模式,策略模式,代理模式,職責鏈模式
代理模式代理顧名思義,就是客服無法直接與本體進行的溝通通過第三方進行轉述。虛擬代理
作為創建開銷大的對象的代表;虛擬代理經常直到我們真正需要一個對象的時候才創建它;當對象在創建前或創建中時,由虛擬代理來扮演對象的替身;對象創建后,代理就會將請求直接委托給對象;圖片預加載例子
const myImage = (function() { const imgNode = document.createElement("img"); document.body.appendChild(imgNode); return { setSrc: function(src) { imgNode.src = src; } } }()) // 代理容器 const proxyImage = (function() { let img = new Image(); // 加載完之后將設置為添加的圖片 img.onload = function() { myImage.setSrc(this.src) } return { setSrc: function(src) { myImage.setSrc("loading.gif"); img.src = src; } } }()) proxyImage.setSrc("file.jpg")
如上:代理容器控制了客戶對MyImage的訪問,并且在過程中加了一些額外的操作。
緩存代理緩存代理可以為一些開銷大的運算結果提供暫時的存儲,在下次運算時,如果傳遞進來的參數跟之前一致,則可以直接返回前面存儲的運算結果。計算乘積例子
// 求乘積函數(專注于自身職責,計算成績,緩存由代理實現) const mult = function() { let a = 1; for (let i = 0, l =arguments.length; i< l; i++){ a = a * arguments[i]; } return a; } // proxyMult const proxyMult = (function() { let cache = {}; return function() { let args = Array.prototype.join.call(arguments, ","); if (args in cache) { return cache[args]; } return cache[arg] = mult.apply(this, arguments); } }()) proxyMult(1, 2, 3) // 6 proxyMult(1, 2, 3) // 6迭代器模式
迭代器模式是指提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表達式。迭代器模式可以將迭代的過程從業務邏輯中分離出來,在使用迭代器模式之后,即不用關心對象內部構造也可以按順序訪問其中的每個元素。
迭代器分為內部迭代器和外部迭代器,內部迭代器,是在函數內部已經定義好了迭代規則,外部只需要調用即可。但如果要修改需求,那就要去迭代函數內部去修改邏輯了。外部迭代器指的是必須顯示的請求迭代下一個元素。
如現在有一個需求,判斷兩個函數是否完全相等,分別使用內部迭代器和外部迭代器去實現。
// 使用內部迭代的方式實現 compare const compare = function (arr1, arr2) { try { if (arr1.length !== arr2.length) { throw "arr1和arr2不相等" } // forEach 相當一于一個迭代器 arr1.forEach((item, index) => { if (item !== arr2[index]) { throw "arr1和arr2不相等" } }) console.log("arr1等于arr2") } catch (e) { console.log(e) } }
使用外部迭代器模式改寫compare
// 迭代器 const iterator = function (obj) { let current = 0; let next = function () { current += 1; }; let isDone = function () { return current >= obj.length; } let getCurrItem = function () { return obj[current]; } return { next: next, isDone: isDone, getCurrItem: getCurrItem, length: obj.length } } // 重寫compare const compare = function (iterator1, iterator2) { try { if (iterator1.length !== iterator2.length) { throw "iterator1不等于iterator2" } while (!iterator1.isDone() && !iterator2.isDone()) { if (iterator1.getCurrItem() !== iterator2.getCurrItem()) { throw "iterator1不等于iterator2" } iterator1.next(); iterator2.next(); } console.log("iterator1 === iterator2") } catch (e) { console.log(e) } } const iterator1 = iterator([1, 2, 3]); const iterator2 = iterator([1, 2, 3]); compare(iterator1, iterator2)
迭代器實際場景的應用
根據不同的瀏覽器獲取相應上傳組件對象
// 常規寫法 const getUploadObj = function() { try { return new ActiveXObject("txftna") } catch (e) { if (supportFlash()) { let str = `` return document.body.appendChild(str) } else { let str = ``; return document.body.appendChild(str); } } } // 迭代模式改寫 // IE上傳控件 const getActiveUploadObj = function () { try { return new ActiveXObject("TXFTNActiveX.FTNUPload") } catch (e) { return false; } } // flash上傳控件 const getFlashUploadObj = function () { if (supportFlash()) { let str = `` return document.body.appendChild(str) } return false } // 表單上傳 const getFormUploadObj = function () { let str = ``; return document.body.appendChild(str); } // 使用迭器執行 const iteratorUploadObj = function () { for (let i = 0, fn; fn = arguments[i++];) { const uploadInstane = fn() if (uploadInstane !== false) { return uploadInstane } } } iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)
觀察上面經過未使用迭代器模式,和使用迭代器模式后,發現不使用的話,如果后期想新增一個其他的模式,需要修改原來代碼的邏輯,如果使用迭代器的模式的話只需要新增一個方法即可。雖然在寫的時候代碼多了幾行,但總的來說,后期擴展以及可閱讀性明顯提高了~。
單例模式定義:單例模式指的是保證一個類僅有一個實例,且提供一個訪問它的全局訪問點。全局緩存,window對象,都可以看作是一個單例。
目的: 解決一個全局使用的類頻繁地創建與銷毀
使用ES6 class 創建單例:class Instance { static init () { if (!this.instance) { this.instance = new Instance() } return this.instance; } } const instance1 = Instance.init() const instance2 = Instance.init() console.log(instance1 === instance2) //true使用閉包創建單例:
const instance = (function() { let instance = null; return function(name) { if (!instance) { instance = new Singleton(name) } return instance; } }())使用代理實現單例模式
以在頁面上創建唯一的dom節點為例;
// 創建div類 class CreateDiv { constructor(html) { this.html = html this.init() } init() { let div = document.createElement("div"); div.innerHTML = this.html; document.body.appendChild(div) } } // 代理類 class ProxySingletonCreateDiv { static initInstance(html) { if (!this.instance) { this.instance = new CreateDiv(html) } return this.instance; } } ProxySingletonCreateDiv.initInstance("test1"); ProxySingletonCreateDiv.initInstance("test2");
測試上面這段代碼會發現頁面上只會顯示test1,因為實例只創建了一次,也就是new CreateDiv只執行了第一次,第二次并沒有執行。
惰性單例惰性單例是指在需要的時候才創建對象實例。(在一定場景下,用戶只有在需要的時候才創建)
例:instance實例總是在我們調用getInstance的時候才會被創建
Singleton.getInstance = (function () { let instance = null; return function (name) { if (!instance) { instance = new Singleton(name) } return instance; } }()) const instance = Singleton.getInstance("hello")
實現通用惰性單例
const getSingle = function (fn) { let result; return function() { return result || (result = fn.apply(this, arguments)) } }裝飾者模式
裝飾者模式指的是:可以動態地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其他對象。最基礎的裝飾者
以編寫一個飛機大戰的游戲為例,隨著等級的增加最開始我們只能發送普通的子彈,二級可以發送導彈,三級可以發送原子彈。
// 飛機對象 let plane = { fire: function () { console.log("發射普通子彈") } }; // 發送導彈的方法,實際內容略 let missileDecorator = function () { console.log("發射導彈"); }; // 發送原子彈的方法 let atomDecorator = function () { console.log("發射原子彈"); }; // 將發送子彈方法存起來, let fire1 = plane.fire; // 裝飾發送普通子彈的方法 plane.fire = function () { fire1(); missileDecorator() }; let fire2 = plane.fire; plane.fire = function () { fire2(); atomDecorator() }; plane.fire() // 發射普通子彈,發射導彈,發射原子彈
我們有時候在維護代碼的時候,可能會遇到這樣的需求。比如給window綁定onload事件,但又不太確定這個事件是否被其他人綁定過了,為了避免覆蓋掉之前的window.onload的函數的行為,我們一般會向上面的例子一樣,將之前的行為保存在一個變量內,然后再給綁定的window.onload函數添加這個變量執行從而滿足需求。
以上方法的缺陷:
需要多維護了中間變量,如上面的例子fire1和fire2,如果鏈越來越長,那么維護的就越來越多。
還會遇到this劫持問題,如上fire函數被變量存起來的時候plane.fire執行時this指向global(node環境)
AOP裝飾函數為解決上面this的劫持問題,延伸實現Function.prototype.before和Function.prototype.after方法:
Function.prototype.before = function (beforeFn) { let that = this; // 保存原函數的引用 return function () { beforeFn.apply(this, arguments); return that.apply(this, arguments); //執行原函數,且保證this不被劫持 } }; Function.prototype.after = function (afterFn) { let that = this; return function () { let ret = that.apply(this, arguments); afterFn.apply(this, arguments); return ret; } };
改寫上面飛機的例子:
let plane = { fire: function () { console.log("發射普通子彈!") } }; plane.fire.after(function () { console.log("發射導彈!") }).after(function () { console.log("發射原子彈!") })()
很明顯的看見解決了上面的兩個缺陷。
AOP應用實例之數據上報做前端開發,主要提升用戶體驗,所以有時候在項目結尾為了能更多的收集到用戶的操作數據不得不加入一些埋點數據在業務中。如:點擊上報多少人點擊登錄按鈕來顯示登錄浮窗。
// 常規做法 bad let log = function () { console.log("上報")// 實際內容略 } let showLogin = function () { console.log("打開登錄浮窗"); log(); } // 上面做法,showLogin既要做顯示彈窗的操作,又要負責數據上報,違反了單一職責原則 // 使用裝飾者方式改寫 good let showLogin1 = function () { console.log("顯示彈窗") } let showLogin = showLogin.after(log); document.getElement("button").onclick = showLogin;
裝飾者模式與代碼模式的區別:
代理模式強調的是代理與它的實體之間的關系(這種關系在一開始就可以被確定),裝飾者模式用于一開始無法確定對象的全部功能場景。代理模式通常只有一層代理。而裝飾者會形成一條長長的裝飾鏈。
中介者指的是解除對象與對象之間的緊耦關系,增加中介者之后,所有對象通過中介者來通信,而不是互相引用,所以當一個對象發生改變時,只需要通知中介者對象即可。
現實生活中很的中介者場景,如:快遞物流公司,快遞員將負責的區域拿到用戶的快遞之后將快遞送到中轉場,然后中轉場再進行整理之后輸出分好類的區域,再由快遞員送到指定區域。在設計模式中,中轉場就扮演者,中介者的角色。
如圖將 A,B,C,D互相關聯的東西使用一個中介者進行管理,減少A,B,C,D內的互相引用。
const createAgent = (function () { return { add() => { //添加一個東西 代碼略 }, send () => { // 發送一個東西 代碼略 } } }()) const createA = function () { createAgent.add() setTimeout(() => { createAgent.send(); // 代碼略 }, 3000) } const createB = function () { //同上面方法類似 }
代碼只是說明中介者的意圖,內容不要在意。
當關聯的東西越來越多的時候中介者模式會變得越來越大,雖然會帶來這個缺點,但取舍一下還是會比相互之間引用會更好。
發布-訂閱模式發布-訂閱模式:定義對象之間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都將得到通知!
常見的javascript事件指令,也是一種發布訂閱模式,
document.body.addEventListener("click", function () { // 一堆操作 }, false) // 模擬用戶點擊 document.body.click()
這里監控用戶點擊document.body的動作,但我們并不知道用戶什么時候會點擊,所以我們訂閱document.body上的click事件,當body被點擊后,body節點會向訂閱者發布這個消息。
發布與訂閱模式的實現步驟:
首先要指定誰充當發布者。
然后給發布者添加一個緩存列表,用于存放回調函數以便通知訂閱者
最后發布消息的時候,發布者會便利緩存列表依次觸發里面存放的訂閱者回調函數
以售樓處去購買房子為例:A去售樓部查看了一下房源,并告知了銷售者小姐姐自己的電話以及自己需要的房源類型(這里A充當訂閱者),售樓處將用戶的電話以及需要房源類型記錄在小本子上(這個小本子相當于緩存列表),售樓處充當發布者!下面以一段代碼實現這段描述:
// 定義售樓處 let saleOffices = { // 存放訂閱者的回調函數,即用戶的電話以及需求 clientList: {}, // 訂閱消息 listen: function (key, fn) { // 如果沒有訂閱過此類消息,則創建一個緩存列表 if (!this.clientList[key]) { this.clientList[key] = []; } // 將訂閱的消息添加進消息緩存列表 this.clientList[key].push(fn); }, // 發布消息 trigger: function () { let key = Array.prototype.shift.call(arguments ); let fns = this.clientList[key]; // 如果沒訂閱此消息則返回 if (!fns || fns.length === 0) return false; for (let i = 0, fn; fn = fns[i++];) { // arguments是發布消息的附送參數 fn.apply(this, arguments); } }, // 取消訂閱 remove: function (key, fn) { let fns = this.clientList[key]; if (!fns) return false; // 如果沒有傳入回調函數,則取消key所有的訂閱 if (!fn) { fns && (fns.length = 0) } else { for (let l = fns.length - 1; l >= 0; l--) { let _fn = fns[l]; if (_fn === fn) { // 刪除訂閱的回調函數 fns.splice(l, 1) } } } } }; let fn1 = function (price) { console.log("價格=", price) } saleOffices.listen("listen100", fn1) saleOffices.remove("listen100", fn1) saleOffices.trigger("listen100", 20000)
看了發布與訂閱模式和中介者模式,發現兩者之間有著很多相似之處。
發布-訂閱與中介者之間的區別:
中介者目的是為了減少對象之間的耦合,而且類里的內容可能存在著不同對象之間需要的一些東西存儲,后期可能是某個對象自己去取。發布-訂閱主要也是解決對象之間的耦合,不同的是發布訂閱是取決用戶關注什么東西后發布者在有了這個東西之后主動推送給訂閱者~模板方法模式
模板模式指的是一種只需要使用繼承就可以實現的非常簡單的模式 ,比較依賴于抽象類的一種設計模式,主要由抽象父類和具體實現子類組成!
抽象類可以表示一種契約,繼承了這個抽象類的所有子類都將擁有跟抽象類一致的接口方法,抽象類的主要作用就是為了它子類定義這些公共接口!
咖啡與茶的例子泡茶與沖咖啡:
首先可以將茶和咖啡抽象成飲料。
兩個在泡和沖都有相似的步驟:
把水煮沸
用沸水沖泡飲料
把飲料倒進杯子
加調料
在類的繼承情況下,有時會存在子類未實現父類里已經調用過的一些方法,常見解決可以在父類里添加對象的方法并提示一個錯誤如:
Beverage.prototype.brew = function () { throw new Error("子類必須重寫brew方法"); }
實現沖咖啡與泡茶的例子:
// 抽象類 class Beverage { boilWater () { console.log("把水煮沸"); } brew () { throw new Error("子類必須實現此方法!") } pourInCup () { throw new Error("子類必須實現此方法!") } addCondiments () { throw new Error("子類必須實現此方法!") } // 構子方法是否需要添加調料 customerWantsCondiments() { return true } init () { this.boilWater(); this.brew(); this.pourInCup(); if (this.customerWantsCondiments()) { this.addCondiments(); } } } // 咖啡類 class Coffee extends Beverage{ constructor(props) { super(props) } brew() { console.log("用沸水煮咖啡") } pourInCup() { console.log("把咖啡倒進杯子") } addCondiments() { console.log("加糖和牛奶") } customerWantsCondiments () { return false; } } // 泡茶類 class Tea extends Beverage { constructor(props) { super(props); } brew () { console.log("用沸水浸泡茶葉") } pourInCup () { console.log("將茶水倒進杯子") } addCondiments () { console.log("加對應配料") } } let coffee = new Coffee() coffee.init() // 把水煮沸 用沸水煮咖啡 把咖啡倒進杯子 let tea = new Tea() tea.init(); // 把水煮沸 用沸水浸泡茶葉 將茶水倒進杯子 加對應配料好萊塢原則
許多新人演員在好萊塢把簡歷遞給演藝公司之后就只有回家等待盡管。有時候演員等得不耐煩了,給演藝公司打電話詢問情況,演藝公司往往這樣回答:“不要來差我,我會給你打電話。”,這就是好萊塢原則。
好萊塢原則,允許底層組件將自己掛鉤到高層組件中,而高層組件會決定什么時候,什么方式使用這些底層組件,與好萊塢原則一樣。
在javascript中,我們很多時候不需要用類這樣繁瑣的方式實現模板方法,使用高階函數更好。我們用高階函數改寫上面的例子。
const Beverage = function(param) { let boilWater = function() { console.log("把水煮沸") } let brew = param.brew || function() { throw new Error("必須傳遞brew方法") } let pourInCup = param.pourInCup || function() { throw new Error("必須傳遞pourInCup方法") } let addCondiments = param.addCondiments || function() { throw new Error("必須傳遞addCondiments") } let F = function () {}; F.prototype.init = function() { boilWater(); brew(); pourInCup(); addCondiments() } return F; } const Coffee = Beverage({ brew:function() { console.log("用沸水沖泡咖啡") }, pourInCup: function() { console.log("把咖啡倒進杯子") }, addCondiments: function() { console.log("加糖和牛奶") } })策略模式
策略模式指的是:定義一系列算法,把它們一個個封裝起來,并且使它們可以互相替換。
策略模式優點:
策略模式例用組合、委托和多態等技術和思想,可以有效地避免多重條件選擇語句
策略模式提供了對開放-封閉原則的完美支持,將算法封裝在獨立的strategy中,使得它們易于切換,易于理解,易于擴展。
策略模式中的算法也可以復用在系統的其他地方,從而避免許多重復的復制粘貼工作。
在策略模式中利用組合和委托來讓context擁有執行算法的能力,這也是繼承的一種更輕便的替代方案。
表單校驗例子校驗邏輯:
用戶名不能為空,用戶名長度不能小于10位
密碼長度不能少于6位
手機號必須符合格式。
為了下面的javascript代碼更少的編寫html代碼,我這里將html代碼提取出來
// html 代碼
不使用策略模式我們正常的實現
const registerForm = document.getElementById("registerForm"); registerForm.onsubmit = function() { const userName = registerForm.userName.value if(userName === "" && userName.length >= 10) { console.log("用戶名不能為空") return false; } if (registerForm.password.value.length < 6) { console.log("密碼不能為空") return false; } if (!/(^1[3|5|8][0|9]{9}$)/.test(registerForm.phoneNumber.value)) { console.log("手機號輸入不正確") return false; } }
這樣的代碼會導致校驗的函數越來越龐大,在系統變化的時候缺乏彈性。
使用策略模式重構上面的表單校驗:
策略模式的組成部分:
策略類:封裝具體算法,并負責具體計算過程。
環境類:環境類Context,Context接受客戶的請求,隨后把請求委托給某一個策略類。
// 封裝算法 const strategies = { isNonEmpty: function (value, errorMsg) { if (value === "") { return errorMsg; } }, minLength: function (value, length, errorMsg) { if (value.length < length) { return errorMsg; } }, isMobile: function (value, errorMsg) { if (!/(^1[3|5|8][0|9]{9}$)/.test(value)) { return errorMsg; } } } // 實現Context環境類 const Validator = function () { this.cache = []; } Validator.prototype = { add (dom, rules) { let self = this; for (let i = 0, rule; rule = rules[i++];) { let strategyAry = rule.strategy.split(":"); let errorMsg = rule.errorMsg; self.cache.push(function () { let strategy = strategyAry.shift() strategyAry.unshift(dom.value) strategyAry.push(errorMsg); return strategies[strategy].apply(dom, strategyAry) }) } }, start () { for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) { let errorMsg = validatorFunc(); if (errorMsg) { return errorMsg; } } } } // 客戶端使用 let registerForm = document.getElementById("registerForm"); let validataFunc = function () { let validator = new Validator(); validator.add(registerForm.userName, [{ strategy: "isNonEmpty", errorMsg: "用戶名不能為空" }, { strategy: "minLength:10", errorMsg: "用戶名長度不能小于10位" }]) validator.add(registerForm.password, [{ strategy: "minLength:6", errorMsg: "密碼長度不能小于6位" }]) validator.add(registerForm.phoneNumber, [{ strategy: "isMobile", errorMsg: "手機號碼格式不正確" }]) let errorMsg = validator.start(); return errorMsg; } registerForm.onsubmit = function () { let errorMsg = validataFunc(); if (errorMsg) { console.log("errorMsg"); return false; } }
雖然看起來代碼多了很多,但對于以后的維護和擴展方法,復用方法,這種方式明顯會好很多。
職責鏈模式職責鏈模式指的是:使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關系,將這些對象連成一個鏈條,并沿著鏈條傳遞這些請求,直到有一個對象處理它為止。
職責鏈生活例子:
中學時期末考試,如果你平時不老實考試時就會被安排到第一排的位置,遇到不會答的題就會把題寫在紙條上傳給后排的同學,如果后排同樣不會繼續往后排傳值到會為止。
實際開發中職責鏈模式使用場景:
需求是這樣的:
商場在預定手機時,分別繳納500和200的定金,到的購買階段后交付500定金的可以收到100優惠券,200的可以收到50優惠券,沒有支付定金沒有優惠券且只能保證購買時庫存有貨時能購買成功!
平常我們拿到這樣的需求后可能會寫下面這樣的代碼:
const fn = function(stock) { if (stock > 0) { console.log("普通購買,無優惠券") } else { console.log("手機庫存不足") } } /** * @param {number} orderType 訂單類型1,2,3 * @param {boolean} pay 是否支付定金 true | false * @param {number} stock 庫存數量 */ let order = function(orderType, pay, stock) { if (orderType === 1) { if (pay === true) { console.log("500元定金預購,得到100優惠券。") } else { fn(stock); } } else if (olderType === 2) { if (pay === true) { console.log("200元,50優惠券") } else { fn(stock) } } else if (orderType === 3) { fn(stock) } }
看上面的代碼,邏輯上也沒什么問題,但相信我們在寫的時候一般也不會這樣去寫,因為這樣在后期維護的時候order函數會變得越來越龐大,而且要新增一些其他的邏輯也是比較困難。
使用職責鏈模式重寫上面的例子:
拆分條件語句,將每個條件提取成一個函數。
約定一個字符串"nextSuccess"是否需要向后傳遞
包裝職責鏈chain
const order500 = function(orderType, pay, stock) { if(orderType === 1 && pay === true) { console.log("500定金,100優惠券") } else { return "nextSuccess" } } const order200 = function(orderType, pay, stock) { if (orderType === 2 && pay === true) { console.log("200定金,返50優惠券") } else { return "nextSuccess" } } const orderNormal = function(orderType, pay, stock) { if (stock > 0) { console.log("普通購買") } else { console.log("手機庫存不足") } } const Chain = function(fn) { this.fn = fn; this.successor = null; } Chain.prototype = { setNextSuccessor: function(successor) { return this.successor = successor; }, passRequest: function() { let ret = this.fn.apply(this, arguments); if (ret === "nextSuccessor") { return this.successor && this.successor.passRequest.apply(this.successor, arguments) } return ret; } } // 包裝職責鏈節點 const chainOrder500 = new Chain(order500); const chainOrder200 = new Chain(order200); const chainOrderNormal = new Chain(orderNormal); // 指定職責鏈順序 chainOrder500.setNextSuccessor(chainOrder200); chainOrder200.setNextSuccessor(chainOrderNormal) // 應用 chainOrder500.passRequest(1, true, 500); // 500定金,100優惠券 chainOrder500.passRequest(1, false, 0) // 庫存不足
職責鏈的缺點:
職責鏈使程序中多了一些節點對象,可能在某一次的請求傳遞過程中,大部分節點沒有起到實質性的作用,它們的作用僅僅讓請求傳遞下去,從性能方面考慮我們應該避免過長的職責鏈帶來的性能損耗。
在之前我們使用了AOP裝飾函數,實現裝飾者的模式。
同樣 這里我們可以使用AOP實現職責鏈
改寫之前的Function.prototype.after函數
Function.prototype.after = function(fn) { let self = this; return function() { let ret = self.apply(this, arguments); if (ret === "nextSuccessor") { return fn.apply(this, arguments) } return ret; } } // 指定順序 let order = order500.after(order200).after(orderNormal); order(1, true, 50) // 500定金,100優惠券
去掉了chain類,整個邏輯也變得更加清晰了,同樣這種方式也不適合太長的鏈條。
狀態模式狀態模式指的是:允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。
狀態模式的優點:
狀態模式定義了狀態與行為之間的關系,并將它們封裝在一個類里。通過增加新的狀態類,很容易增加新的狀態和轉換。
避免Context無限膨脹,狀態切換的邏輯被分布在狀態類中,也去掉了Context中原來過多的條件分支。
用對象代替字符串來記錄當前狀態,使得狀態的切換更加一目了然。
Context中的請求動作和狀態類中封裝的行為可以非常容易地獨立變化而互不影響。
使用javascript版本的狀態機,實現開燈與關燈例子:
let delegate = function(client, delegation) { return { buttonWasPressed: function() { // 將客戶端操作委托給delegation對象 return delegation.buttonWasPressed.apply(client, arguments) } } } let FSM = { off: { buttonWasPressed: function() { console.log("關燈") this.button.innerHTML = "下次按我是開燈"; this.currState = this.onState; } }, on: { buttonWasPressed: function() { console.log("開燈"); this.button.innerHTML = "下次按我是關燈" this.currState = this.offState; } } } let Light = function() { this.offState = delegate(this, FSM.off); this.onState = delegate(this, FSM.on); this.currState = this.offState; // 設置初始狀態為關閉狀態 this.button = null; } Light.prototype = { init () { let button = document.getElementById("button"); let self = this; button.innerHTML = "已關燈"; this.button = document.body.appendChild(button); this.button.onclick = function () { self.currState.buttonWasPressed() } } } let light = new Light() light.init()結語
寫這篇文章主要意圖在于以前也看過javascript設計模式這本書,但可能在寫代碼的時候一般只會想到文章開頭的三個原則,但具體如何通過比較優雅的代碼去滿足三個原則是比較困難的,最近又重新看了一遍這本書,為了加深自己的印象,將一些比較常用的模式通過自己去描述的方式呈現出來,也能分享給廣大的搬磚同學~, 如果覺得讀完文章后有所收獲,請點贊和收藏給予一丟丟鼓勵,haha
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/106954.html
摘要:前言月份開始出沒社區,現在差不多月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了一般來說,差不多到了轉正的時候,會進行總結或者分享會議那么今天我就把看過的一些學習資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區,現在差不多9月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了!一般來說,差不多到了轉正的時候,會進行總結或者分享會議!那么今天我就...
摘要:特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 本以為自己收藏的站點多,可以很快搞定,沒想到一入匯總深似海。還有很多不足&遺漏的地方,歡迎補充。有錯誤的地方,還請斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應和斧正,會及時更新,平時業務工作時也會不定期更...
摘要:前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。背后的故事本文是對于年之間世界發生的大事件的詳細介紹,闡述了從提出到角力到流產的前世今生。 前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點;分為新聞熱點、開發教程、工程實踐、深度閱讀、開源項目、巔峰人生等欄目。歡迎...
摘要:忍者級別的函數操作對于什么是匿名函數,這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數是一個很重要且具有邏輯性的特性。通常,匿名函數的使用情況是創建一個供以后使用的函數。 JS 中的遞歸 遞歸, 遞歸基礎, 斐波那契數列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執行機制 本文的目的就是要保證你徹底弄懂javascript的執行機制,如果...
摘要:在他的重學前端課程中提到到現在為止,前端工程師已經成為研發體系中的重要崗位之一。大部分前端工程師的知識,其實都是來自于實踐和工作中零散的學習。一基礎前端工程師吃飯的家伙,深度廣度一樣都不能差。 開篇 前端開發是一個非常特殊的行業,它的歷史實際上不是很長,但是知識之繁雜,技術迭代速度之快是其他技術所不能比擬的。 winter在他的《重學前端》課程中提到: 到現在為止,前端工程師已經成為研...
閱讀 2474·2021-11-23 09:51
閱讀 533·2019-08-30 13:59
閱讀 1836·2019-08-29 11:20
閱讀 2542·2019-08-26 13:41
閱讀 3250·2019-08-26 12:16
閱讀 742·2019-08-26 10:59
閱讀 3336·2019-08-26 10:14
閱讀 607·2019-08-23 17:21