# Day 37：從 while(true) 到選擇適合的迴圈結構

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2026-01-13

---

用 do...while 重構：divideBy3Times 實作
---------------------------------

    /**
     * 計算一個數字需要除以 3 幾次，才能讓小數第二位變成 0
     *
     * @param {number} num - 起始數字
     * @returns {number} 需要除以 3 的次數
     */
    export const divideBy3Times = (num) => {
      let count = 0;
      let secondDecimal = undefined;
    
      do {
        num /= 3;
        count += 1;
        const str = num.toFixed(2);
        secondDecimal = str[str.indexOf(".") + 2];
      } while (secondDecimal !== "0");
    
      return count;
    };
    

**設計重點：**

*   用 **do...while** 表達「至少要除一次,再看要不要繼續」的語意\[1\]\[2\]\[3\]
    
*   `secondDecimal` 只宣告不給實際初始值，表達「真正有意義的值會在第一次迴圈內容裡產生」
    
*   while 條件只判斷一次 `secondDecimal !== "0"`，沒有多餘的 `if + break`
    

* * *

while / do...while / 其他方式的角色分工
------------------------------

### 1\. while：先判斷，再執行

**適合情境：**

*   有可能「一次都不需要執行」
    
*   條件所需的變數，在進入迴圈前就有合理初始值\[4\]\[3\]
    

    let i = 0;
    
    while (i < 5) {
      i += 1;
    }
    

**關鍵語意：**「只要條件為真，就重複做這件事」

### 2\. do...while：先執行，再判斷（至少一次）

**適合情境：**

*   業務語意是「至少要做一次，再看是否繼續」
    
*   條件依賴「第一次迴圈內才算得出來」的值\[2\]\[5\]\[1\]
    

    let value;
    do {
      value = readInput();
    } while (!isValid(value));
    

**關鍵語意：**「一定至少執行一次」

**上述 divideBy3Times 的情境：**

*   題意：一定先除一次，再看第二位小數是不是 0，若不是繼續除
    
*   條件 `secondDecimal` 需要在「做完 `/= 3` + `toFixed(2)`」之後才知道
    
*   用 do...while 剛好符合「先做一次，再判斷」
    

* * *

拆開 while(true) 的常見方法與適用場景
-------------------------

### 1\. 遞迴（Recursion）

**適合情境：**

*   問題天然有遞迴結構（樹、分治）
    
*   重試次數可控，深度不會太大（JS 遞迴太深可能觸發 `Too much recursion`）\[6\]
    

**範例（輸入驗證重試）：**

    async function askWithValidatorUntilPass(prompt, validator) {
      const rl = createRl();
      const raw = await rl.question(prompt);
    
      try {
        const value = validator(raw);
        rl.close();
        return value;
      } catch (error) {
        console.log(error.message);
        rl.close();
        return askWithValidatorUntilPass(prompt, validator);
      }
    }
    

\*\*權衡：\*\*狀態清楚、程式短；但不適合大量迭代

* * *

### 2\. 陣列方法（map / filter / reduce / some / find…）

**適合情境：**

*   已知要處理的「一組資料」
    
*   要做轉換、過濾、累加等集合運算\[7\]
    

    // map
    function doubleAll(numbers) {
      return numbers.map((n) => n * 2);
    }
    
    // some
    function hasNegative(numbers) {
      return numbers.some((n) => n < 0);
    }
    

**權衡：語意很清楚、無副作用；但只適用於已存在的集合**，不適合「模擬直到條件成立」

* * *

### 3\. for / for...of

**適合情境：**

*   遍歷集合
    
*   需要 `break` / `continue` 控制流程
    

    function sumPositive(numbers) {
      let sum = 0;
      for (const num of numbers) {
        if (num > 0) sum += num;
      }
      return sum;
    }
    

\*\*權衡：\*\*比 while 語意更清楚（「正在遍歷某個集合」），但仍是命令式寫法

* * *

### 4\. Generator + for...of

**適合情境：**

*   需要產生序列（甚至可能是無限序列）
    
*   需要延遲計算（lazy evaluation）\[8\]\[9\]
    

    function* range(start, end) {
      let current = start;
      while (current < end) {
        yield current;
        current += 1;
      }
    }
    
    for (const n of range(1, 5)) {
      console.log(n);
    }
    

\*\*權衡：\*\*很適合序列與狀態機，但概念較進階，不適合作為初學者預設選項

* * *

### 5\. 高階函式（Higher-Order Functions）

**適合情境：**

*   把「控制邏輯」抽出來重用，例如重試、重試延遲等
    

    function retry(fn, maxAttempts, attempt = 1) {
      try {
        return fn();
      } catch (error) {
        if (attempt >= maxAttempts) throw error;
        return retry(fn, maxAttempts, attempt + 1);
      }
    }
    

\*\*權衡：\*\*能把邏輯參數化、可重用；但需要適當抽象，不然容易 over-engineer

* * *

### 6\. 提取「終止條件」為獨立函式

**適合情境：**

*   while 條件很長、很難一眼看懂
    
*   終止條件在業務上有名字
    

    function isNonNegative(num) {
      return num >= 0;
    }
    
    function findFirstNegative(numbers) {
      return numbers.find((num) => !isNonNegative(num)) ?? null;
    }
    

\*\*權衡：\*\*可測試、可重用；但多一層間接性，小問題不一定值得

* * *

### 7\. Pipeline（來自《重構》書的 "Replace Loop with Pipeline"）

\*\*補充說明：\*\*這個方法主要來自 Martin Fowler 的《重構》（Refactoring）第二版，書中有專門章節討論「用 pipeline 取代迴圈」\[10\]\[11\]

**適合情境：**

*   **for loop 遍歷已知集合**，要做多步驟的轉換、過濾、累積
    
*   想要用「資料流」的方式表達邏輯（filter → map → reduce）\[12\]\[13\]
    

#### 經典範例（《重構》書中）

    // ❌ for loop 版本
    const names = [];
    for (const person of input) {
      if (person.job === "programmer") {
        names.push(person.name);
      }
    }
    
    // ✅ Pipeline 版本
    const names = input
      .filter((person) => person.job === "programmer")
      .map((person) => person.name);
    

**為什麼更好：**

*   ✅ 資料流清楚：`input` → `filter` → `map` → `names`
    
*   ✅ 每一步的意圖明確（過濾 → 轉換）
    
*   ✅ 可組合、可重用\[12\]
    

#### while loop 可以改成 Pipeline 嗎？

**要看 while loop 在做什麼：**

##### ✅ 適合：while loop 在「產生序列」

    // ❌ while loop 版本
    function getEvenNumbers(max) {
      const result = [];
      let i = 0;
      while (i <= max) {
        if (i % 2 === 0) {
          result.push(i);
        }
        i++;
      }
      return result;
    }
    
    // ✅ Generator + Pipeline 版本
    function* evenNumbers(max) {
      let i = 0;
      while (i <= max) {
        if (i % 2 === 0) yield i;
        i++;
      }
    }
    
    const result = Array.from(evenNumbers(10))
      .filter((n) => n > 0)
      .map((n) => n * 2);
    

##### ⚠ 不一定適合：while loop 在「累積計算」

divideBy3Times 的例子：

    // do...while 版本（已經很清楚）
    let count = 0;
    let secondDecimal;
    do {
      num /= 3;
      count += 1;
      secondDecimal = num.toFixed(2)[num.toFixed(2).indexOf(".") + 2];
    } while (secondDecimal !== "0");
    
    // 改成 pipeline：需要先產生序列，反而更複雜
    function* divisionSequence(num) {
      let current = num;
      while (true) {
        current /= 3;
        const str = current.toFixed(2);
        const secondDecimal = str[str.indexOf(".") + 2];
        yield { current, secondDecimal };
        if (secondDecimal === "0") break;
      }
    }
    const count = Array.from(divisionSequence(2)).length;
    

\*\*結論：\*\*這個情境不適合改成 pipeline，do...while 更直覺

##### ❌ 不適合：while loop 在「模擬/重試」

時間模擬的例子：

    // while loop：模擬時間直到達到目標
    while (time < MAX_TIME) {
      time += 1;
      const runnerState = calculateRunnerState({ ... });
      
      if (runnerState.distance >= targetDistance) {
        return runnerState.time;
      }
    }
    

**為什麼不適合 pipeline：**

*   這是「模擬直到條件成立」，不是「處理一組資料」
    
*   沒有「已知的集合」可以 pipeline
    
*   while loop 更直覺地表達「推進時間，直到達標」的語意
    

#### Pipeline 的核心原則（來自《重構》）

Martin Fowler 強調：**重構的目標是提高可讀性，不是為了套用某種模式**\[11\]\[10\]

#### for vs while 與 Pipeline 的對照

項目

for loop

while loop

**主要用途**

遍歷已知集合

重複直到條件成立

**是否有集合**

✅ 有

❌ 不一定

**Pipeline 適用性**

✅ 高（filter/map/reduce）\[12\]

⚠ 看情況

**《重構》書中**

主要示範對象

較少提及

* * *

什麼情況保留 while 反而是最自然？
--------------------

**時間模擬範例：**

    export function calculateFinishTime({
      targetDistance,
      runnerSpeed,
      breakInterval = 0,
      backwardDistance = 0,
    }) {
      const MAX_TIME = 100000; // 安全上限，避免無限迴圈
      let time = 0;
    
      while (time < MAX_TIME) {
        time += 1;
    
        const runnerState = calculateRunnerState({
          runnerSpeed,
          time,
          breakInterval,
          backwardDistance,
        });
    
        if (runnerState.distance >= targetDistance) {
          return runnerState.time;
        }
      }
    
      throw new Error(
        `模擬超時：跑者無法在 ${MAX_TIME} 秒內完賽（可能參數設定有誤）`,
      );
    }
    

**分析：**

*   問題本質是「**模擬時間往前推，直到距離達標**」
    
*   次數未知，不是遍歷固定集合，也不是自然的遞迴結構
    
*   while 搭配「上限條件 + clear return」語意很直覺\[6\]
    

**設計決策：**

*   保留 while：因為問題本質就是「重複模擬直到達到條件」
    
*   加上 `MAX_TIME`：控制安全性，避免參數設錯導致無限迴圈
    
*   把終止條件寫在 if 裡，用 early return 結束迴圈，讓控制流程簡單
    

* * *

決策樹：選擇 while / do...while / 其他結構
--------------------------------

簡化成一棵「選擇樹」：

    我要處理的是什麼？
    
    1. 遍歷一組已知資料？
       ├─ 要轉換/過濾/累積 → Pipeline（陣列方法 filter/map/reduce）[web:92][web:104]
       └─ 只要走過去，可以中途 break → for...of / for
    
    2. 要重試 / 模擬直到條件成立？
       ├─ 深度小、邏輯遞迴自然 → 遞迴
       ├─ 一定至少執行一次，條件依賴第一次結果 → do...while
       └─ 迭代次數可能很多 → while（加上限）
    
    3. 要產生序列（可能無限）？
       └─ Generator + for...of（可搭配 pipeline）
    
    4. while 似乎無法避免？
       ├─ 條件簡單、可先算好 → while(條件)
       ├─ 條件需要迴圈內值 → 考慮 do...while 或旗標變數
       └─ 必須 while(true) → 確保：
           - 內部有明確的 return / break
           - 有適當的註解說明終止條件
    

* * *

評估 while / do...while / pipeline / 其他的四個維度
------------------------------------------

1.  **可讀性**：不熟的人 10 秒內能不能看懂？
    
2.  **適用性**：是否貼合問題本質？
    
3.  **安全性**：有沒有機會無限迴圈？（while(true) 風險最高，需要 extra 小心）
    
4.  **效能**：是否有真實效能瓶頸？（大多時候 while / for 差異可以忽略）\[6\]
    

* * *

幾個關鍵結論
------

*   while 本身不是壞人，**關鍵是條件要有語意、要安全**
    
*   do...while 是「先做一次，再判斷」的專用工具，**像上述 divideBy3Times 這種場景就很適合**\[3\]\[1\]\[2\]
    
*   時間模擬這種「模擬直到條件成立」的問題，用 while（加上限）是自然且可讀的做法\[6\]
    
*   **Pipeline（來自《重構》書）主要針對 for loop 遍歷已知集合的情境**，while loop 是否適合改成 pipeline 要看它在做什麼\[10\]\[12\]
    
*   拆開 while 的方法很多（遞迴、陣列方法、for、generator、高階函式、pipeline…），但**不要為了「拆而拆」，而是為了更貼近問題本質與提升可讀性**
    

**一句話收斂：**

> **不是要消滅 while，而是要讓每一個 while / do...while 的存在，都有清楚的語意與邊界。Pipeline 是重構 for loop 的強大工具，但不是所有迴圈的萬靈丹。**

* * *

遞迴實作案例：隨機器材搬運
-------------

### 今日進度

✅ **完成項目**

*   題目分析與資料結構設計
    
*   實作隨機選擇器材的遞迴邏輯
    
*   修正遞迴基底條件與狀態傳遞問題
    
*   程式已能正常運作（隨機搬運、不超重停止）
    

🔄 **待完成項目**

*   實作 `countEquipments()` 統計每種器材數量
    
*   格式化輸出（器材名稱、規格、個數、總重量）
    

* * *

### 核心概念

#### 1\. 資料結構設計：扁平化 vs 巢狀結構

**選擇扁平化陣列的原因：**

*   符合「等機率隨機選擇」的需求
    
*   避免 `Object.values() + flatMap()` 的額外轉換
    
*   每個項目包含完整資訊（name, spec, weight）
    

    // ✅ 扁平結構（適合此題）
    const equipments = [
      { name: 'Kettlebell', spec: '6 kg', weight: 6 },
      { name: 'Kettlebell', spec: '8 kg', weight: 8 },
    ];
    
    // 隨機選擇一行搞定
    const randomIndex = Math.floor(Math.random() * equipments.length);
    

**巢狀結構適用情境：**

*   需要依類別分組處理
    
*   需要統計每個類別的數量
    
*   資料更新集中在某個類別
    

* * *

#### 2\. 遞迴設計模式

**遞迴三要素：**

1.  **基底條件（終止條件）**：`nextLoad > maxLoad` 時停止
    
2.  **狀態傳遞**：`currentLoad` 和 `equipmentsList` 作為參數往下傳
    
3.  **遞迴呼叫**：不超重時繼續呼叫自己
    

    export function getTruckLoad(
      equipments,
      maxLoad,
      currentLoad = 0,
      equipmentsList = []
    ) {
      // 1. 執行邏輯（隨機選器材）
      const randomIndex = getRandomInt(0, equipments.length);
      const selectedEquipment = equipments[randomIndex];
      const nextLoad = currentLoad + selectedEquipment.weight;
    
      // 2. 檢查終止條件
      if (nextLoad > maxLoad) {
        return { equipmentsList, currentLoad };
      }
    
      // 3. 累積狀態並遞迴
      equipmentsList.push(selectedEquipment);
      return getTruckLoad(equipments, maxLoad, nextLoad, equipmentsList);
    }
    

**關鍵技巧：**

*   使用預設參數讓呼叫端介面簡潔
    
*   每次遞迴傳遞更新後的狀態（`nextLoad`）
    
*   避免無意義的基底條件（如 `if (currentLoad <= maxLoad)`）
    

* * *

#### 3\. 遇到的問題與解法

##### 問題 1：基底條件邏輯錯誤

    // ❌ 錯誤：第一次執行就會直接回傳空陣列
    if (currentLoad <= maxLoad) return { equipmentsList, currentLoad };
    

**原因：**

*   `currentLoad = 0`（初始值）永遠 `<= maxLoad`
    
*   第一行就 return，後續程式碼都不執行
    

**解法：**

*   移除這個錯誤的基底條件
    
*   讓 `nextLoad > maxLoad` 當唯一的終止條件
    

* * *

##### 問題 2：遞迴時回傳函式而非呼叫函式

    // ❌ 錯誤：回傳函式本身
    return getTruckLoad;
    
    // ✅ 正確：呼叫函式
    return getTruckLoad(equipments, maxLoad, nextLoad, equipmentsList);
    

* * *

##### 問題 3：陣列索引超出範圍

    // ❌ 錯誤：equipments 有 14 個元素（索引 0-13）
    const randomIndex = getRandomInt(0, equipments.length); // 可能產生 14
    
    // ✅ 正確
    const randomIndex = getRandomInt(0, equipments.length - 1);
    

或直接用標準 `Math.random()` 寫法：

    const randomIndex = Math.floor(Math.random() * equipments.length);
    

* * *

### 程式碼品質討論

#### Mutable vs Immutable 陣列操作

**方案 1：直接修改（push）**

    equipmentsList.push(selectedEquipment);
    return getTruckLoad(equipments, maxLoad, nextLoad, equipmentsList);
    

**優點：**

*   直覺、效能稍好
    
*   在確定不會複用陣列的情境下沒問題
    

**缺點：**

*   有副作用（會修改傳入的陣列）\[14\]
    
*   如果外部傳入陣列，原陣列會被改變
    

* * *

**方案 2：建立新陣列（展開運算子）**

    const nextEquipmentsList = [...equipmentsList, selectedEquipment];
    return getTruckLoad(equipments, maxLoad, nextLoad, nextEquipmentsList);
    

**優點：**

*   無副作用，更安全\[15\]
    
*   函式可被安全複用
    
*   符合函數式編程原則
    

**缺點：**

*   需要理解展開運算子語法
    
*   每次建立新陣列有微小效能成本
    

* * *

**方案 3：concat（等同展開運算子）**

    const nextEquipmentsList = equipmentsList.concat(selectedEquipment);
    

**特性：**

*   不修改原陣列
    
*   語意清楚但較不常見
    

* * *

#### 選擇建議

情境

推薦做法

簡單練習題、不會複用

`push`（此例採用）

函式庫、會被多處呼叫

`[...array, item]`

需要避免副作用的函式

`[...array, item]` 或 `concat`

**本例決策：** 維持 `push` 做法，因為是單一用途的練習題，邏輯清楚且不會複用陣列。

* * *

### 統計函式設計（待實作）

    /**
     * 統計器材清單中每種器材的數量
     * @param {Array} equipmentsList - 器材陣列
     * @returns {Array} 統計後的陣列，格式：[{ name, spec, weight, count }, ...]
     */
    export function countEquipments(equipmentsList) {
      const countMap = {};
    
      equipmentsList.forEach((item) => {
        const key = `${item.name}-${item.spec}`;
        
        if (countMap[key]) {
          countMap[key].count += 1;
        } else {
          countMap[key] = {
            name: item.name,
            spec: item.spec,
            weight: item.weight,
            count: 1,
          };
        }
      });
    
      return Object.values(countMap);
    }
    

**練習重點：**

*   Object 當作 Map 使用
    
*   `Object.values()` 轉陣列
    
*   Template Literal 組合 key
    

* * *

### 格式化輸出設計

**期望輸出：**

    車上器材清單：
    1. Kettlebell (6 kg) x 3
    2. Weight Plates (10 kg) x 5
    3. Dumbbell (10 kg) x 2
    
    總重量：998 kg / 1000 kg
    

* * *

學習收穫
----

1.  **資料結構設計直接影響實作複雜度**：扁平化結構讓隨機選擇變得簡單
    
2.  **遞迴需要明確的終止條件和狀態傳遞**：錯誤的基底條件會讓遞迴立刻結束或無限循環
    
3.  **Array 的可變性需要權衡**：在簡單情境可用 `push`，複雜情境建議用不可變方法\[14\]\[15\]
    
4.  **預設參數讓遞迴介面更友善**：外部呼叫不用管內部狀態
    

* * *

這份筆記記錄了從 `while(true)` 到選擇適合迴圈結構的思考過程，以及遞迴、Pipeline、do...while 等不同方法的適用場景。重點在於理解每種結構的語意，根據問題本質選擇最直覺、最安全的寫法。

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-37)*
