程序猿的本職工做就是寫代碼,寫出高質量的代碼應該是咱們的追求和對本身的要求,由於:javascript
- 高質量的代碼每每意味着更少的BUG,更好的模塊化,是咱們擴展性,複用性的基礎
- 高質量的代碼也意味着更好的書寫,更好的命名,有利於咱們的維護
怎樣來定義代碼質量的"好",業界有不少標準,本文認爲好的代碼應該有如下特色:前端
- 代碼整潔,好比縮進之類的,如今有不少工具能夠自動解決這個問題,好比eslint。
- 結構規整,沒有漫長的結構,函數拆分合理,不會來一個幾千行的函數,也不會有幾十個
if...else
。這要求寫代碼的人有一些優化的經驗,本文會介紹幾種模式來優化這些狀況。- 閱讀起來好理解,不會出現一堆
a,b,c
這種命名,而是應該儘可能語義化,變量名和函數名都儘可能有意義,最好是代碼即註釋,讓別人看你的代碼就知道你在幹嗎。
本文介紹的設計模式主要有策略/狀態模式
,外觀模式
,迭代器模式
,備忘錄模式
。java
假如咱們須要作一個計算器,須要支持加減乘除,爲了判斷用戶具體須要進行哪一個操做,咱們須要4個if...else
來進行判斷,若是支持更多操做,那if...else
會更長,不利於閱讀,看着也不優雅。因此咱們能夠用策略模式優化以下:ios
function calculator(type, a, b) { const strategy = { add: function(a, b) { return a + b; }, minus: function(a, b) { return a - b; }, division: function(a, b) { return a / b; }, times: function(a, b) { return a * b; } } return strategy[type](a, b); } // 使用時 calculator('add', 1, 1);
上述代碼咱們用一個對象取代了多個if...else
,咱們須要的操做都對應這個對象裏面的一個屬性,這個屬性名字對應咱們傳入的type
,咱們直接用這個屬性名字就能夠獲取對應的操做。git
狀態模式和策略模式很像,也是有一個對象存儲一些策略,可是還有一個變量來存儲當前的狀態,咱們根據當前狀態來獲取具體的操做:github
function stateFactor(state) { const stateObj = { status: '', state: { state1: function(){}, state2: function(){}, }, run: function() { return this.state[this.status]; } } stateObj.status = state; return stateObj; } // 使用時 stateFactor('state1').run();
if...else
實際上是根據不一樣的條件來改變代碼的行爲,而策略模式和狀態模式均可以根據傳入的策略或者狀態的不一樣來改變行爲,全部咱們能夠用這兩種模式來替代if...else
。axios
這個例子的需求是咱們的頁面須要根據不一樣的角色來渲染不一樣的內容,若是咱們用if...else
寫就是這樣:設計模式
// 有三個模塊須要顯示,不一樣角色看到的模塊應該不一樣 function showPart1() {} function showPart2() {} function showPart3() {} // 獲取當前用戶的角色,而後決定顯示哪些部分 axios.get('xxx').then((role) => { if(role === 'boss'){ showPart1(); showPart2(); showPart3(); } else if(role === 'manager') { showPart1(); showPart2(); } else if(role === 'staff') { showPart3(); } });
上述代碼中咱們經過API請求得到了當前用戶的角色,而後一堆if...else
去判斷應該顯示哪些模塊,若是角色不少,這裏的if...else
就可能很長,咱們能夠嘗試用狀態模式優化下:數組
// 先把各類角色都包裝到一個ShowController類裏面 function ShowController() { this.role = ''; this.roleMap = { boss: function() { showPart1(); showPart2(); showPart3(); }, manager: function() { showPart1(); showPart2(); }, staff: function() { showPart3(); } } } // ShowController上添加一個實例方法show,用來根據角色展現不一樣的內容 ShowController.prototype.show = function() { axios.get('xxx').then((role) => { this.role = role; this.roleMap[this.role](); }); } // 使用時 new ShowController().show();
上述代碼咱們經過一個狀態模式改寫了訪問權限模塊,去掉了if...else
,並且不一樣角色的展現都封裝到了roleMap
裏面,後面要增長或者減小都會方便不少。緩存
這個例子的需求是咱們如今有一個小球,咱們須要控制他移動,他移動的方向能夠是上下左右,還能夠是左上,右下之類的複合運動。若是咱們也用if...else
來寫,這頭都會寫大:
// 先來四個方向的基本運動 function moveUp() {} function moveDown() {} function moveLeft() {} function moveRight() {} // 具體移動的方法,能夠接收一個或兩個參數,一個就是基本操做,兩個參數就是左上,右下這類操做 function move(...args) { if(args.length === 1) { if(args[0] === 'up') { moveUp(); } else if(args[0] === 'down') { moveDown(); } else if(args[0] === 'left') { moveLeft(); } else if(args[0] === 'right') { moveRight(); } } else { if(args[0] === 'left' && args[1] === 'up') { moveLeft(); moveUp(); } else if(args[0] === 'right' && args[1] === 'down') { moveRight(); moveDown(); } // 後面還有不少if... } }
能夠看到這裏if...else
看得咱們頭都大了,仍是用策略模式來優化下吧:
// 建一個移動控制類 function MoveController() { this.status = []; this.moveHanders = { // 寫上每一個指令對應的方法 up: moveUp, dowm: moveDown, left: moveLeft, right: moveRight } } // MoveController添加一個實例方法來觸發運動 MoveController.prototype.run = function(...args) { this.status = args; this.status.forEach((move) => { this.moveHanders[move](); }); } // 使用時 new MoveController().run('left', 'up')
上述代碼咱們也是將全部的策略都封裝到了moveHanders
裏面,而後經過實例方法run
傳入的方法來執行具體的策略。
當咱們設計一個模塊時,裏面的方法能夠會設計得比較細,可是暴露給外面使用的時候,不必定非得直接暴露這些細小的接口,外部使用者須要的多是組合部分接口來實現某個功能,咱們暴露的時候其實就能夠將這個組織好。這就像餐廳裏面的菜單,有不少菜,用戶能夠一個一個菜去點,也能夠直接點一個套餐,外觀模式提供的就相似於這樣一個組織好的套餐:
function model1() {} function model2() {} // 能夠提供一個更高階的接口,組合好了model1和model2給外部使用 function use() { model2(model1()); }
外觀模式提及來其實很是常見,不少模塊內部都很複雜,可是對外的接口可能都是一兩個,咱們無需知道複雜的內部細節,只須要調用統一的高級接口就行,好比下面的選項卡模塊:
// 一個選項卡類,他內部可能有多個子模塊 function Tab() {} Tab.prototype.renderHTML = function() {} // 渲染頁面的子模塊 Tab.prototype.bindEvent = function() {} // 綁定事件的子模塊 Tab.prototype.loadCss = function() {} // 加載樣式的子模塊 // 對外不須要暴露上面那些具體的子模塊,只須要一個高級接口就行 Tab.prototype.init = function(config) { this.loadCss(); this.renderHTML(); this.bindEvent(); }
上述代碼這種封裝模式很是常見,其實也是用到了外觀模式,他固然也能夠暴露具體的renderHTML
,bindEvent
,loadCss
這些子模塊,可是外部使用者可能並不關心這些細節,只須要給一個統一的高級接口就行,就至關於改變了外觀暴露出來,因此叫外觀模式
。
這個例子也很常見,就是把一些相似的功能封裝成一個方法,而不是每一個地方去寫一遍。在之前仍是IE主導天下的時候,咱們須要作不少兼容的工做,僅僅是一個綁定事件就有addEventListener
,attachEvent
,onclick
等,爲了不每次都進行這些檢測,咱們能夠將他們封裝成一個方法:
function addEvent(dom, type, fn) { if(dom.addEventListener) { return dom.addEventListener(type, fn, false); } else if(dom.attachEvent) { return dom.attachEvent("on" + type, fn); } else { dom["on" + type] = fn; } }
而後將addEvent
暴露出去給外面使用,其實咱們在實際編碼時常常這樣封裝方法,只是咱們本身可能沒意識到這個是外觀模式。
迭代器模式模式在JS裏面很常見了,數組自帶的forEach
就是迭代器模式的一個應用,咱們也能夠實現一個相似的功能:
function Iterator(items) { this.items = items; } Iterator.prototype.dealEach = function(fn) { for(let i = 0; i < this.items.length; i++) { fn(this.items[i], i); } }
上述代碼咱們新建了一個迭代器類,構造函數接收一個數組,實例方法dealEach
能夠接收一個回調,對實例上的items
每一項都執行這個回調。
其實JS數組不少原生方法都用了迭代器模式,好比find
,find
接收一個測試函數,返回符合這個測試函數的第一個數據。這個例子要作的是擴展這個功能,返回全部符合這個測試函數的數據項,並且也能夠接收兩個參數,第一個參數是屬性名,第二個參數是值,一樣返回全部該屬性與值匹配的項:
// 外層用一個工廠模式封裝下,調用時不用寫new function iteratorFactory(data) { function Iterator(data) { this.data = data; } Iterator.prototype.findAll = function(handler, value) { const result = []; let handlerFn; // 處理參數,若是第一個參數是函數,直接拿來用 // 若是不是函數,就是屬性名,給一個對比的默認函數 if(typeof handler === 'function') { handlerFn = handler; } else { handlerFn = function(item) { if(item[handler] === value) { return true; } return false; } } // 循環數據裏面的每一項,將符合結果的塞入結果數組 for(let i = 0; i < this.data.length; i++) { const item = this.data[i]; const res = handlerFn(item); if(res) { result.push(item) } } return result; } return new Iterator(data); } // 寫個數據測試下 const data = [{num: 1}, {num: 2}, {num: 3}]; iteratorFactory(data).findAll('num', 2); // [{num: 2}] iteratorFactory(data).findAll(item => item.num >= 2); // [{num: 2}, {num: 3}]
上述代碼封裝了一個相似數組find
的迭代器,擴展了他的功能,這種迭代器很是適合用來處理API返回的大量結構類似的數據。
備忘錄模式相似於JS常用的緩存函數,內部記錄一個狀態,也就是緩存,當咱們再次訪問的時候能夠直接拿緩存數據:
function memo() { const cache = {}; return function(arg) { if(cache[arg]) { return cache[arg]; } else { // 沒緩存的時候先執行方法,獲得結果res // 而後將res寫入緩存 cache[arg] = res; return res; } }
這個例子在實際項目中也比較常見,用戶每次點進一個新文章都須要從API請求數據,若是他下次再點進同一篇文章,咱們可能但願直接用上次請求的數據,而再也不次請求,這時候就能夠用到咱們的備忘錄模式了,直接拿上面的結構來用就好了:
function pageCache(pageId) { const cache = {}; return function(pageId) { // 爲了保持返回類型一致,咱們都返回一個Promise if(cache[pageId]) { return Promise.solve(cache[pageId]); } else { return axios.get(pageId).then((data) => { cache[pageId] = data; return data; }) } } }
上述代碼用了備忘錄模式來解決這個問題,可是代碼比較簡單,實際項目中可能需求會更加複雜一些,可是這個思路仍是能夠參考的。
這個例子的需求是,咱們須要作一個能夠移動的DIV,用戶把這個DIV隨意移動,可是他有時候可能誤操做或者反悔了,想把這個DIV移動回去,也就是將狀態回退到上一次,有了回退狀態的需求,固然還有配對的前進狀態的需求。這種相似的需求咱們就能夠用備忘錄模式實現:
function moveDiv() { this.states = []; // 一個數組記錄全部狀態 this.currentState = 0; // 一個變量記錄當前狀態位置 } // 移動方法,每次移動記錄狀態 moveDiv.prototype.move = function(type, num) { changeDiv(type, num); // 僞代碼,移動DIV的具體操做,這裏並未實現 // 記錄本次操做到states裏面去 this.states.push({type,num}); this.currentState = this.states.length - 1; // 改變當前狀態指針 } // 前進方法,取出狀態執行 moveDiv.prototype.forward = function() { // 若是當前不是最後一個狀態 if(this.currentState < this.states.length - 1) { // 取出前進的狀態 this.currentState++; const state = this.states[this.currentState]; // 執行該狀態位置 changeDiv(state.type, state.num); } } // 後退方法是相似的 moveDiv.prototype.back = function() { // 若是當前不是第一個狀態 if(this.currentState > 0) { // 取出後退的狀態 this.currentState--; const state = this.states[this.currentState]; // 執行該狀態位置 changeDiv(state.type, state.num); } }
上述代碼經過一個數組將用戶全部操做過的狀態都記錄下來了,用戶能夠隨時在狀態間進行前進和後退。
本文講的這幾種設計模式策略/狀態模式
,外觀模式
,迭代器模式
,備忘錄模式
都很好理解,並且在實際工做中也很是常見,熟練使用他們能夠有效減小冗餘代碼,提升咱們的代碼質量。
策略模式
經過將咱們的if
條件改寫爲一條條的策略減小了if...else
的數量,看起來更清爽,擴展起來也更方便。狀態模式
跟策略模式
很像,只是還多了一個狀態,能夠根據這個狀態來選取具體的策略。外觀模式
可能咱們已經在無心間使用了,就是將模塊一些內部邏輯封裝在一個更高級的接口內部,或者將一些相似操做封裝在一個方法內部,從而讓外部調用更加方便。迭代器模式
在JS數組上有不少實現,咱們也能夠模仿他們作一下數據處理的工做,特別適合處理從API拿來的大量結構類似的數據。備忘錄模式
就是加一個緩存對象,用來記錄以前獲取過的數據或者操做的狀態,後面能夠用來加快訪問速度或者進行狀態回滾。本文是講設計模式的最後一篇文章,前面三篇是:
文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
本文素材來自於網易高級前端開發工程師微專業唐磊老師的設計模式課程。
做者博文GitHub項目地址: https://github.com/dennis-jiang/Front-End-Knowledges