iT邦幫忙

2025 iThome 鐵人賽

DAY 9
1
Software Development

消除你程式碼的臭味系列 第 9

Day 9- 迴圈最佳化:把邊界判斷和特殊處理移到外面

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 9- 迴圈最佳化:把邊界判斷和特殊處理移到外面

一個迴圈的本質工作,就是對一系列相似的東西做同樣一件事情。

當你在迴圈裡面塞滿了邊界判斷和特殊處理,邏輯就會變亂,也更容易出錯。

先做好準備工作,將所有例外、邊界情況處理好。
然後,讓迴圈專心、單純地執行「主要工作」。

經典案例:混雜的列表渲染

把特殊處理塞在迴圈裡。

// 🔴 臭味道:把設置、清理、主要心邏輯全混在一起
function renderList(items) {
  let output = '';
  for (let i = 0; i < items.length; i++) {
    // 這些檢查跟迴圈的核心任務有什麼關係?沒有。
    if (!Array.isArray(items)) return ''; 
    if (items[i] == null) continue;

    // 每次迭代都要問「我是不是第一個?」
    if (i === 0) output += 'START\n'; 

    output += `- ${items[i]}\n`; // 這才是迴圈真正該做的事

    // 每次迭代也都要問「我是不是最後一個?」
    if (i === items.length - 1) output += 'END\n'; 
  }
  return output;
}

https://ithelp.ithome.com.tw/upload/images/20250911/20124462HAkHV9fFZF.png

  • 混淆職責: 這段程式碼把「邊界處理」(START/END)和「資料處理」(- ${items[i]})攪和在一起。一個函式、一個迴圈,應該只做一件事。

  • 極度低效: i === 0 的判斷只在第一次為真,但它會在迴圈的一百萬個元素中,執行一百萬次。i === items.length - 1 同樣如此。浪費 CPU 時間去做重複且無意義的檢查。

  • 可讀性災難: 真正的主要邏輯 output += ... 被一堆 if 包圍,難以閱讀。

把邊界與特殊處理移到外面

除臭第一步就是知道如何分離問題。
先處理好所有邊界情況和特殊輸入,然後用一個乾淨、簡單的迴圈來處理主要工作。
建立清晰的資料處理管道,將整個過程分解成幾個獨立的步驟:驗證 -> 清理 -> 處理 -> 組合

// 🟢 好品味:先處理邊界,再專心處理主要工作
function renderList(items) {
  // 1. 保護式檢查 (Guard Clause):把所有不要的輸入先擋掉
  if (!Array.isArray(items) || items.length === 0) {
    return '';
  }

  // 2. 準備資料:先把 null 過濾掉,只留下我們要處理的
  const cleanItems = items.filter(x => x != null);
  if (cleanItems.length === 0) {
      return '';
  }

  // 3. 主要工作:迴圈只做一件事
  const body = cleanItems.map(x => `- ${x}`).join('\n');

  // 4. 組合結果:把邊界和主要工作拼起來
  return `START\n${body}\nEND`;
}

https://ithelp.ithome.com.tw/upload/images/20250911/201244620Hx8RUqldB.png

  • 消除特殊情況: 透過 filter,迴圈主體 map 根本不需要知道 null 的存在。它處理的每一個元素都是「正常情況」。這就是重點。

  • 清晰的資料流 pipe: items -> cleanItems -> body -> final result。每一步都只做一件事。你可以輕易地測試任何一個環節。

  • 關注點分離:每個區塊只做一件事。修改驗證邏輯不會動到核心處理,反之亦然。

需要知道的權衡 (Trade-off):可讀性 vs 記憶體

.filter().map() 寫法會產生暫存陣列,因此會多用一點記憶體。
相比之下,傳統 for 迴圈則最節省記憶體。

那該如何選擇?結論很簡單:在絕大多數情況下,都應該優先選擇可讀性最高的寫法。
只有在遇到效能瓶頸極端情況才考慮另一種方案。

經典案例:避免在迴圈內分支

// 🔴 壞味道:在迴圈裡重複做一個不會改變的決定
function applyDiscount(items, level) {
  const result = [];
  for (const item of items) {
    // `level` 在迴圈中從未改變,但這個判斷卻執行了 N 次。
    if (level === 'vip') {
      result.push(item.price * 0.8);
    } else if (level === 'member') {
      result.push(item.price * 0.9);
    } else {
      result.push(item.price);
    }
  }
  return result;
}

把不變的判斷提到迴圈外,讓主體邏輯一行完成。折扣率的決策與迴圈的迭代無關。

// 🟢 好品味:先把「決策」提到迴圈外,只做一次
function applyDiscount(items, level) {
  // 1. 提取不變量:先根據 level 決定折扣率
  const rate = level === 'vip' ? 0.8 : level === 'member' ? 0.9 : 1;

  // 2. 應用決策:讓迴圈專心執行計算
  return items.map(item => item.price * rate);
}

這個改進不只效能好,而且更清楚。

https://ithelp.ithome.com.tw/upload/images/20250911/20124462biBwim9Mo6.png

迴圈的意圖變成單純的「將每個元素的價格乘以一個比率」,而不是「為每個元素檢查會員等級再決定折扣」。

常用技巧

  • 衛兵先行: 在迴圈開始前,用保護式子句處理掉所有無效輸入(null、空陣列等)。

  • 清理資料: 在迴圈開始前,先把資料預處理好。過濾掉無效項,讓進入迴圈的都是乾淨、格式統一的資料。

  • 提取不變量: 任何在迴圈內不會改變的計算或判斷,都提到迴圈外面。

  • 迴圈要笨: 讓迴圈主體只做最核心、重複性的那件事。

  • 事後處理: 首、尾元素的特殊邏輯,應該在迴圈執行前和執行後處理。

今日重點

  • 迴圈應該只做一件事:專注於處理「正常情況」。
  • 把型別檢查、空陣列處理、首尾特例移到外面。
  • 不變條件放到外部決定,別在每次迭代重新判斷。
  • 用更直觀的資料流操作,讓意圖一眼可見。

一個好的迴圈,它的主體應該只處理第 N 個元素,而不是為第 0 個或第 N-1 個元素增加特殊邏輯。

好的程式碼不是沒有特殊情況,而是懂得如何聰明地「隔離」它們,讓主要情況與邏輯保持單純與穩定。


上一篇
Day 8- 消除抽象層:直接存取資料,不要繞路
下一篇
Day 10- 介面最小化:只暴露必要的東西
系列文
消除你程式碼的臭味12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言