## 第二章 簡化迴圈與邏輯 目的:讓程式易於理解,減少程式中的「心理負擔 (mental baggage)」 --- ## 提高控制流程可讀性 讓所有條件判斷、迴圈與其他改變流程的敘述「盡量自然」 ### 1. 條件式中條件順序 A. ``` if ( length >= 10 ) // good ``` ``` if ( 10 <= length ) // bad ``` B. ``` if( bytes_received < bytes_expected ) ``` ``` if( bytes_expected > bytes_received ) ``` 通用原則: | 左側 | 右側 | | -------- | -------- | |「比較對象表示式」,數值較有變化 |「比較對象基準式」,數值為固定常數 | ### 2. if / else 區塊順序 在不考慮巢狀結構情況下,避免難以閱讀的順序呈現,以下條件有助於決定區塊順序: > 這些條件偶有牴觸,但沒有牴觸時都是可遵循原則 * 先處理**肯定**條件而非**否定**條件 * 先處理**簡單**的情況 -> 先處理邏輯簡單或者判斷條件少的情況 * 先處理比較**有趣**或**明顯**的情況 -> 先處理那些在業務邏輯中比較重要或者用戶最關心的情況 ### 3. 有效使用三元運算子 縮短其他人理解程式所需時間優先度高於減少程式行數;只在簡單情況下使用三元運算子 A. ``` time_str += (hour >= 12) ? 'pm' : 'am' ``` B. ``` const accessLevel = user.isActive ? user.age > 18 ? 'full' : 'restricted' : 'none'; ``` ### 4. early return 透過「把守條件(guard clauses)」儘早由函數中返回,提高程式碼可讀性並降低複雜性 ### 5. 減少巢狀結構 多層巢狀除了難懂以外,每多一層巢狀結構就需要使用者將更多環境資訊「推進堆疊」,盡可能使用「線性」程式流程,避免巢狀結構 Before: 不同的條件式的表達方式不一致,部分與 SUCCESS 比較另外與非SUCCESS比較 ``` if (user_result === SUCCESS) { if(permission_result !== SUCCESS) { reply.error("error reading permissions") reply.done() return } reply.error("") } else { reply.error(user_result) } reply.done() ``` After: 先處理失敗情況並 early return > 修改程式碼要以全新角度審視,退一步以整體的角度考慮程式碼 ``` if (user_result !== SUCCESS) { reply.error(user_result) reply.done() return } if(permission_result !== SUCCESS) { reply.error("error reading permissions") reply.done() return } reply.error("") reply.done() ``` --- ## 分解巨大表示式 將巨大表示分解為更容易消化的大小 ### 1. 解釋性變數( explaining variable ) 解釋變數主要是為了可讀性和明確性,用於使條件或複雜表達式使其變得清晰 Before: ``` if ( line.split(:)[0].strip() === 'root' ) {...} ``` After: ``` const userName = line.split(:)[0].strip() if( userName === 'root' ) {...} ``` ### 2. 摘要變數( summary variable ) 這類變數目的是用較短、較易於瞭解的名稱取代較大的程式碼,通常保存資料或結果的計算值,有助於提高效率 Before: ``` if ( request.user.id === document.owned_id ) {...} ... if ( request.user.id !== document.owned_id) {...} ``` After: 雖然表示式不算大,但包含五個變數增加了理解成本;利用加入摘要變數除了能易於理解也能清楚表示出「使用者是否擁有這份文件」概念 ``` const isUserOwnsDocument = request.user.id === document.owned_id if (isUserOwnsDocument) {...} ... if (!isUserOwnsDocument) {...} ``` ### 3. 利用笛摩根定律 笛摩根定律包括兩個基本的轉換原則: * NOT (A AND B) 等於 (NOT A) OR (NOT B) * NOT (A OR B) 等於 (NOT A) AND (NOT B) Before: ``` if (!(user.isActive && user.hasPaid)) ``` After: 利用笛摩根定律讓布林值表示式更有可讀性 ``` if (!user.isActive || !user.hasPaid) ``` ### 4. 分解巨大的敘述 程式每個敘述單獨看都不大,但集合起來有機會成為一個巨大敘述體,如同範例程式碼,觀察後可以發現許多敘述都相同,可以抽取出來成為摘要變數 Before: ``` var update_highlight = function (message_num) { if ($("#vote_value" + message_num).html() === "Up") { $("#thumbs_up" + message_num).addClass("highlighted"); $("#thumbs_down" + message_num).removeClass("highlighted"); } else if ($("#vote_value" + message_num).html() === "Down") { $("#thumbs_up" + message_num).removeClass("highlighted"); $("#thumbs_down" + message_num).addClass("highlighted"); } else { $("#thumbs_up" + message_num).removeClass("highighted"); $("#thumbs_down" + message_num).removeClass("highlighted"); } }; ``` After: ``` var update_highlight = function (message_num) { var thumbs_up = $("#thumbs_up" + message_num); var thumbs_down = $("#thumbs_down" + message_num); var vote_value = $("#vote_value" + message_num).html(); var hi = "highlighted"; if (vote_value === "Up") { thumbs_up.addClass(hi); thumbs_down.removeClass(hi); } else if (vote_value === "Down") { thumbs_up.removeClass(hi); thumbs_down.addClass(hi); } else { thumbs_up.removeClass(hi); thumbs_down.removeClass(hi); } }; ``` --- ## 變數與可讀性 在「分解巨大表示」利用**解釋性**與**摘要**變數提高程式碼的可讀性,但濫用變數也將造成以下問題: * 變數越多越難記住所有變數 * 變數存活範圍越大,就必須記得越久 * 變數越常改變,越難記得目前的數值 ### 1. 消除變數 消除程式碼中對可讀性沒有幫助的變數 #### 不必要的暫存變數 ``` const now = Date.now() lastViewTime = now ``` `now` 不是必要變數原因: * 不是分解複雜表示式的結果 * 程式沒有更加明確 * 只使用一次,沒有消除任合重複程式碼 結論:不使用 `now` 程式碼一樣容易理解 ``` lastViewTime = Date.now() ``` #### 消除中間結果 Before: ``` var remove_one = function (array, value_to_remove) { var index_to_remove = null; for (var i = 0; i < array.length; i += 1) { if (array[i] === value_to_remove) { index_to_remove = i; break; } } if (index_to_remove !== null) { array.splice(index_to_remove, 1); } } ``` After: 移除只是用來保存中間的結果的 `index_to_remove` 變數 ``` var remove_one = function (array, value_to_remove) { for (var i = 0; i < array.length; i += 1) { if (array[i] === value_to_remove) { array.splice(i, 1); return; } } } ``` #### 消除控制流程變數 減少使用額外的變數來控制流程 Before: ``` function findElement(arr, target) { let found = false; let i = 0; while (i < arr.length && !found) { if (arr[i] === target) { found = true; } else { i++; } } return found; } ``` After: ``` function findElement(arr, target) { for (let i = 0; i < arr.length; i++) { if (arr[i] === target) { return true; // 直接返回,消除了控制流變數 } } return false; } ``` ### 2. 縮限變數的範圍 不限於全域變數,對所有變數而言「縮限範圍」都是很好的作法,將變數的作用域限制在它們被使用的最小可能範圍內。 > 盡可能減少可以看到變數的程式碼行數 Before: ``` let submitted = false const submit = function (formName) { if(submitted) return ... submitted = true } ``` After:將變數放在閉包中 ``` const submit = (function () { let submitted = false return function(formName) { if(submitted) return ... submitted = true } }()) ``` ### 3. 偏好單次寫入的變數 盡量使用常數,或是減少變數的改變次數也有很大幫助 > 操作變數地方越多,越難記得變數目前的數值 --- refer: https://mcusoft.wordpress.com/wp-content/uploads/2015/04/the-art-of-readable-code.pdf