# Day 37:從 while(true) 到選擇適合的迴圈結構 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-01-13 **URL:** https://paragraph.com/@gcake/day-37 ## Content 用 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 + breakwhile / 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-engineer6. 提取「終止條件」為獨立函式適合情境: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:這是「模擬直到條件成立」,不是「處理一組資料」沒有「已知的集合」可以 pipelinewhile loop 更直覺地表達「推進時間,直到達標」的語意Pipeline 的核心原則(來自《重構》)Martin Fowler 強調:重構的目標是提高可讀性,不是為了套用某種模式[11][10]for vs while 與 Pipeline 的對照項目for loopwhile 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 / 其他的四個維度可讀性:不熟的人 10 秒內能不能看懂?適用性:是否貼合問題本質?安全性:有沒有機會無限迴圈?(while(true) 風險最高,需要 extra 小心)效能:是否有真實效能瓶頸?(大多時候 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. 遞迴設計模式遞迴三要素:基底條件(終止條件):nextLoad > maxLoad 時停止狀態傳遞:currentLoad 和 equipmentsList 作為參數往下傳遞迴呼叫:不超重時繼續呼叫自己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 學習收穫資料結構設計直接影響實作複雜度:扁平化結構讓隨機選擇變得簡單遞迴需要明確的終止條件和狀態傳遞:錯誤的基底條件會讓遞迴立刻結束或無限循環Array 的可變性需要權衡:在簡單情境可用 push,複雜情境建議用不可變方法[14][15]預設參數讓遞迴介面更友善:外部呼叫不用管內部狀態這份筆記記錄了從 while(true) 到選擇適合迴圈結構的思考過程,以及遞迴、Pipeline、do...while 等不同方法的適用場景。重點在於理解每種結構的語意,根據問題本質選擇最直覺、最安全的寫法。 ## Publication Information - [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/): Publication homepage - [All Posts](https://paragraph.com/@gcake/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gcake): Subscribe to updates