線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
<100 subscribers
線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
/**
* 計算一個數字需要除以 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
適合情境:
有可能「一次都不需要執行」
條件所需的變數,在進入迴圈前就有合理初始值[4][3]
let i = 0;
while (i < 5) {
i += 1;
}
關鍵語意:「只要條件為真,就重複做這件事」
適合情境:
業務語意是「至少要做一次,再看是否繼續」
條件依賴「第一次迴圈內才算得出來」的值[2][5][1]
let value;
do {
value = readInput();
} while (!isValid(value));
關鍵語意:「一定至少執行一次」
上述 divideBy3Times 的情境:
題意:一定先除一次,再看第二位小數是不是 0,若不是繼續除
條件 secondDecimal 需要在「做完 /= 3 + toFixed(2)」之後才知道
用 do...while 剛好符合「先做一次,再判斷」
適合情境:
問題天然有遞迴結構(樹、分治)
重試次數可控,深度不會太大(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);
}
}
**權衡:**狀態清楚、程式短;但不適合大量迭代
適合情境:
已知要處理的「一組資料」
要做轉換、過濾、累加等集合運算[7]
// map
function doubleAll(numbers) {
return numbers.map((n) => n * 2);
}
// some
function hasNegative(numbers) {
return numbers.some((n) => n < 0);
}
權衡:語意很清楚、無副作用;但只適用於已存在的集合,不適合「模擬直到條件成立」
適合情境:
遍歷集合
需要 break / continue 控制流程
function sumPositive(numbers) {
let sum = 0;
for (const num of numbers) {
if (num > 0) sum += num;
}
return sum;
}
**權衡:**比 while 語意更清楚(「正在遍歷某個集合」),但仍是命令式寫法
適合情境:
需要產生序列(甚至可能是無限序列)
需要延遲計算(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);
}
**權衡:**很適合序列與狀態機,但概念較進階,不適合作為初學者預設選項
適合情境:
把「控制邏輯」抽出來重用,例如重試、重試延遲等
function retry(fn, maxAttempts, attempt = 1) {
try {
return fn();
} catch (error) {
if (attempt >= maxAttempts) throw error;
return retry(fn, maxAttempts, attempt + 1);
}
}
**權衡:**能把邏輯參數化、可重用;但需要適當抽象,不然容易 over-engineer
適合情境:
while 條件很長、很難一眼看懂
終止條件在業務上有名字
function isNonNegative(num) {
return num >= 0;
}
function findFirstNegative(numbers) {
return numbers.find((num) => !isNonNegative(num)) ?? null;
}
**權衡:**可測試、可重用;但多一層間接性,小問題不一定值得
**補充說明:**這個方法主要來自 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 在做什麼:
// ❌ 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);
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 (time < MAX_TIME) {
time += 1;
const runnerState = calculateRunnerState({ ... });
if (runnerState.distance >= targetDistance) {
return runnerState.time;
}
}
為什麼不適合 pipeline:
這是「模擬直到條件成立」,不是「處理一組資料」
沒有「已知的集合」可以 pipeline
while loop 更直覺地表達「推進時間,直到達標」的語意
Martin Fowler 強調:重構的目標是提高可讀性,不是為了套用某種模式[11][10]
項目 | for loop | while loop |
|---|---|---|
主要用途 | 遍歷已知集合 | 重複直到條件成立 |
是否有集合 | 有 | 不一定 |
Pipeline 適用性 | 高(filter/map/reduce)[12] | ⚠️ 看情況 |
《重構》書中 | 主要示範對象 | 較少提及 |
時間模擬範例:
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 結束迴圈,讓控制流程簡單
簡化成一棵「選擇樹」:
我要處理的是什麼?
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
- 有適當的註解說明終止條件
可讀性:不熟的人 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() 統計每種器材數量
格式化輸出(器材名稱、規格、個數、總重量)
選擇扁平化陣列的原因:
符合「等機率隨機選擇」的需求
避免 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);
巢狀結構適用情境:
需要依類別分組處理
需要統計每個類別的數量
資料更新集中在某個類別
遞迴三要素:
基底條件(終止條件):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))
// ❌ 錯誤:第一次執行就會直接回傳空陣列
if (currentLoad <= maxLoad) return { equipmentsList, currentLoad };
原因:
currentLoad = 0(初始值)永遠 <= maxLoad
第一行就 return,後續程式碼都不執行
解法:
移除這個錯誤的基底條件
讓 nextLoad > maxLoad 當唯一的終止條件
// ❌ 錯誤:回傳函式本身
return getTruckLoad;
// ✅ 正確:呼叫函式
return getTruckLoad(equipments, maxLoad, nextLoad, equipmentsList);
// ❌ 錯誤: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);
方案 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 做法,因為是單一用途的練習題,邏輯清楚且不會複用陣列。
/**
* 統計器材清單中每種器材的數量
* @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 等不同方法的適用場景。重點在於理解每種結構的語意,根據問題本質選擇最直覺、最安全的寫法。
/**
* 計算一個數字需要除以 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
適合情境:
有可能「一次都不需要執行」
條件所需的變數,在進入迴圈前就有合理初始值[4][3]
let i = 0;
while (i < 5) {
i += 1;
}
關鍵語意:「只要條件為真,就重複做這件事」
適合情境:
業務語意是「至少要做一次,再看是否繼續」
條件依賴「第一次迴圈內才算得出來」的值[2][5][1]
let value;
do {
value = readInput();
} while (!isValid(value));
關鍵語意:「一定至少執行一次」
上述 divideBy3Times 的情境:
題意:一定先除一次,再看第二位小數是不是 0,若不是繼續除
條件 secondDecimal 需要在「做完 /= 3 + toFixed(2)」之後才知道
用 do...while 剛好符合「先做一次,再判斷」
適合情境:
問題天然有遞迴結構(樹、分治)
重試次數可控,深度不會太大(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);
}
}
**權衡:**狀態清楚、程式短;但不適合大量迭代
適合情境:
已知要處理的「一組資料」
要做轉換、過濾、累加等集合運算[7]
// map
function doubleAll(numbers) {
return numbers.map((n) => n * 2);
}
// some
function hasNegative(numbers) {
return numbers.some((n) => n < 0);
}
權衡:語意很清楚、無副作用;但只適用於已存在的集合,不適合「模擬直到條件成立」
適合情境:
遍歷集合
需要 break / continue 控制流程
function sumPositive(numbers) {
let sum = 0;
for (const num of numbers) {
if (num > 0) sum += num;
}
return sum;
}
**權衡:**比 while 語意更清楚(「正在遍歷某個集合」),但仍是命令式寫法
適合情境:
需要產生序列(甚至可能是無限序列)
需要延遲計算(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);
}
**權衡:**很適合序列與狀態機,但概念較進階,不適合作為初學者預設選項
適合情境:
把「控制邏輯」抽出來重用,例如重試、重試延遲等
function retry(fn, maxAttempts, attempt = 1) {
try {
return fn();
} catch (error) {
if (attempt >= maxAttempts) throw error;
return retry(fn, maxAttempts, attempt + 1);
}
}
**權衡:**能把邏輯參數化、可重用;但需要適當抽象,不然容易 over-engineer
適合情境:
while 條件很長、很難一眼看懂
終止條件在業務上有名字
function isNonNegative(num) {
return num >= 0;
}
function findFirstNegative(numbers) {
return numbers.find((num) => !isNonNegative(num)) ?? null;
}
**權衡:**可測試、可重用;但多一層間接性,小問題不一定值得
**補充說明:**這個方法主要來自 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 在做什麼:
// ❌ 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);
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 (time < MAX_TIME) {
time += 1;
const runnerState = calculateRunnerState({ ... });
if (runnerState.distance >= targetDistance) {
return runnerState.time;
}
}
為什麼不適合 pipeline:
這是「模擬直到條件成立」,不是「處理一組資料」
沒有「已知的集合」可以 pipeline
while loop 更直覺地表達「推進時間,直到達標」的語意
Martin Fowler 強調:重構的目標是提高可讀性,不是為了套用某種模式[11][10]
項目 | for loop | while loop |
|---|---|---|
主要用途 | 遍歷已知集合 | 重複直到條件成立 |
是否有集合 | 有 | 不一定 |
Pipeline 適用性 | 高(filter/map/reduce)[12] | ⚠️ 看情況 |
《重構》書中 | 主要示範對象 | 較少提及 |
時間模擬範例:
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 結束迴圈,讓控制流程簡單
簡化成一棵「選擇樹」:
我要處理的是什麼?
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
- 有適當的註解說明終止條件
可讀性:不熟的人 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() 統計每種器材數量
格式化輸出(器材名稱、規格、個數、總重量)
選擇扁平化陣列的原因:
符合「等機率隨機選擇」的需求
避免 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);
巢狀結構適用情境:
需要依類別分組處理
需要統計每個類別的數量
資料更新集中在某個類別
遞迴三要素:
基底條件(終止條件):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))
// ❌ 錯誤:第一次執行就會直接回傳空陣列
if (currentLoad <= maxLoad) return { equipmentsList, currentLoad };
原因:
currentLoad = 0(初始值)永遠 <= maxLoad
第一行就 return,後續程式碼都不執行
解法:
移除這個錯誤的基底條件
讓 nextLoad > maxLoad 當唯一的終止條件
// ❌ 錯誤:回傳函式本身
return getTruckLoad;
// ✅ 正確:呼叫函式
return getTruckLoad(equipments, maxLoad, nextLoad, equipmentsList);
// ❌ 錯誤: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);
方案 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 做法,因為是單一用途的練習題,邏輯清楚且不會複用陣列。
/**
* 統計器材清單中每種器材的數量
* @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 等不同方法的適用場景。重點在於理解每種結構的語意,根據問題本質選擇最直覺、最安全的寫法。
Share Dialog
Share Dialog
No comments yet