想像你在處理一份上千筆的訂單名單,要先篩選合格、再計算總額、最後批次打 API 通知倉庫。看起來只是「重複做事」,其實每一步的需求都不同:有的要早停、有的要轉換、有的要並行還要顧配額。
這就是為什麼 JS 有那麼多迴圈寫法。這篇會用貼近實務的方式帶你走一圈:什麼時候用 for 才能乾脆地 break;什麼時候 map/filter/reduce 能讓意圖一眼看懂;遇到非同步,如何在 for…of 裡妥當地 await,又如何用 Promise.all 提速但不失控。
我也會把常見雷區攤給你看,從 forEach + await 到倒序刪除避免索引亂跳。讀完,你會有一套穩、快、好讀的迴圈心法,明天就能用在案子裡。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
為什麼要在意「迴圈」
在任何規模的 JavaScript 專案裡,迴圈都會頻繁出現:整理清單、累計數據、逐筆呼叫 API、掃描 DOM 節點、串接資料轉換⋯⋯。
看似簡單,但一個不留神就會踩到效能、正確性或可維護性的坑。這篇從「有哪些迴圈」、「何時用哪一種」、「怎麼寫最穩」出發,配上逐步操作與常見錯誤對照,讓你快速形成一套可複用的寫法。
JS 迴圈家族地圖
基礎語法型
1. for (初始化; 條件; 遞增):經典索引迴圈,可精準掌控索引、步幅與中斷。
2. while (條件):條件為真就持續,常用於「直到」,例如讀到結束記號。
3. do { ... } while (條件):至少執行一次,之後再判斷。
4. for...in:列舉物件的可列舉屬性鍵(含原型鏈上可列舉者,易踩雷)。
5. for...of:遍歷**可迭代(iterable)**的值(陣列、Map、Set、字串、生成器等)。
陣列高階方法(迭代器語意)
forEach(副作用)、map(對應產生新陣列)、filter(過濾)、reduce(歸納)、some/every(條件檢查)、find/findIndex(搜尋)。
特色:語意清楚、可鏈式撰寫、可讀性好;但不可用 break/continue、forEach 不會等待 await。
其他結構
Object.keys/values/entries 搭配 for...of 或陣列方法處理物件。
Map / Set 專用迭代(順序、內容與物件不同)。
生成器(Generator)與自訂可迭代物件(進階)。
選擇建議(何時用哪一種)
需要索引或要原地修改陣列:for 索引迴圈。
要逐項非同步且「要等待」:for...of + await(或 for 索引搭 await)。
只做資料轉換/過濾:map、filter、reduce 可讀性最佳。
遍歷物件屬性:Object.entries(obj) + for...of(或搭配陣列方法);避免直接用 for...in。
要中途退出:for / while / for...of 可 break / continue。
需要並行發送多個 Promise:用 map 產生 Promise 陣列後 Promise.all,或採批次(chunk)控制。
基礎語法與範例
1. for 索引迴圈
const arr = [10, 20, 30];
for (let i = 0; i < arr.length; i++) {
console.log(i, arr[i]);
}
何時用:需要 i、需要原地改寫、需要可控步幅(如 i += 2)。
步驟化寫法:
1. 明確變數:用 let i = 0。
2. 明確上界:i < arr.length(避免 <=)。
3. 明確步幅:i++ 或 i += step。
4. 需要早停就 break。
常見雷點:
i <= arr.length 導致存取 arr[arr.length] 炸掉(undefined)。
在迴圈中動態 arr.length--/++ 易失控(改動遍歷對象的長度)。
使用 var 造成閉包陷阱。
2. while 與 do...while
let n = 0;
while (n < 3) {
console.log(n);
n++;
}
let m = 0;
do {
console.log(m);
m++;
} while (m < 3);
何時用:條件主導、或需要「至少一次」。
步驟化寫法:
先確定會變化的狀態(避免無窮迴圈)。
迴圈內務必更新使條件終將為假。
常見雷點:
忘記更新條件變數 → 無窮迴圈。
do...while 至少執行一次,若不想落空,請先檢查初始狀態。
3. for...in vs for...of
// for...in:鍵
const obj = {a: 1, b: 2};
for (const k in obj) {
console.log(k, obj[k]);
}
// for...of:值(需要 iterable)
const arr = [1, 2, 3];
for (const v of arr) {
console.log(v);
}
關鍵差異:
for...in 針對「鍵」且會列舉原型鏈可列舉屬性(危)。
上方的(危)是提醒:for...in 預設會把原型鏈上的可列舉屬性掃進來,在物件和陣列上都很容易出現意料外的鍵。除非你真的需要列舉整個原型鏈,否則改用 Object.entries(...) + for...of 或陣列方法,比較安全、可預期。
for...of 針對「值」,僅作用於 iterable(陣列、字串、Map、Set⋯)。
建議:
遍歷陣列用 for...of,遍歷物件用 Object.entries。
若非必要,少用 for...in;如用,請配合 Object.hasOwn(obj, k) 或 Object.prototype.hasOwnProperty.call(obj, k) 篩掉原型鏈。
陣列高階方法:語意導向的迭代
1. forEach(副作用)
[1,2,3].forEach((v, i) => {
console.log(i, v);
});
特色:可讀、適合「逐項做事」。
注意:無法 break/continue;await 在 forEach 內不會等待。
2. map(對應轉換)
const out = [1,2,3].map(x => x * 2); // [2,4,6]
用途:產生新陣列,不改原資料。
反例:只想做副作用請用 forEach,別用 map。
3. filter(過濾)
const evens = [1,2,3,4].filter(x => x % 2 === 0); // [2,4]
4. reduce(歸納)
const sum = [1,2,3].reduce((acc, x) => acc + x, 0); // 6
常見用法:累加、分組、扁平化。
5. some / every / find
const hasEven = [1,3,4].some(x => x % 2 === 0); // true const allEven = [2,4,6].every(x => x % 2 === 0); // true const firstEven = [1,3,4,6].find(x => x % 2 === 0); // 4
選擇原則:以意圖命名的函式讓讀者「不用讀程式就懂你要幹嘛」。
物件、Map、Set 的正確遍歷
物件
const user = { id: 1, name: "Ada" };
for (const [k, v] of Object.entries(user)) {
console.log(k, v);
}
為什麼不用 for...in:避免原型鏈屬性混入。
Map(保持插入順序)
const m = new Map([["a",1], ["b",2]]);
for (const [k, v] of m) {
console.log(k, v);
}
Set(無重複)
const s = new Set([1,2,2,3]);
for (const v of s) {
console.log(v); // 1,2,3
}
非同步迴圈:正確等待與並行策略
1. 逐項「順序」執行(需要等待前一項完成)
async function runSequential(tasks) {
for (const t of tasks) {
const result = await doAsync(t);
console.log(result);
}
}
為什麼用 for...of:它會尊重 await,逐項等完再跑下一項。
2. 「並行」執行(彼此互不相依)
async function runParallel(tasks) {
const promises = tasks.map(t => doAsync(t));
const results = await Promise.all(promises);
console.log(results);
}
適用:IO 密集、互不相依的工作;注意速率限制與 API 配額。
3. 批次並行(控制同時併發數)
async function runInBatches(tasks, size = 5) {
for (let i = 0; i < tasks.length; i += size) {
const batch = tasks.slice(i, i + size).map(t => doAsync(t));
const results = await Promise.all(batch);
console.log(results);
}
}
好處:兼顧速度與風險控管。
常見雷點:
在 forEach 裡用 await:不會等待,流程很亂。
想中途早停卻用 forEach:無法 break。
本可並行卻逐項 await:整體過慢。
並行過度:觸發 API 限流、網路尖峰、記憶體壓力。
迴圈中的控制流程:break、continue、標籤(少用)
for (let i = 0; i < 10; i++) {
if (i === 5) break; // 直接跳出整個迴圈
if (i % 2 === 0) continue; // 略過這一圈,繼續下一圈
console.log(i);
}
標籤迴圈(能跳出外層,但可讀性差,除非明確需要,盡量不用)
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === j) break outer; // 跳出 outer
}
}
實務設計模式(步驟化)
模式 A:累加器(Accumulate)
情境:計算總和、組合字串、聚合統計
let total = 0; for (const n of nums) total += n; // or const total2 = nums.reduce((a, n) => a + n, 0);
步驟
1. 準備初值。
2. 迴圈累加。
3. 輸出結果(或回傳)。
模式 B:搜尋(Search / Early Exit)
情境:找到第一個符合條件的元素就停止
let target = null;
for (const item of items) {
if (ok(item)) { target = item; break; }
}
// or
const target2 = items.find(ok);
重點:需要早停時,優先 for/for...of 或 find。
模式 C:原地過濾(In-place Filter)
情境:大型陣列、避免額外配置
let write = 0;
for (let read = 0; read < arr.length; read++) {
if (keep(arr[read])) {
arr[write] = arr[read];
write++;
}
}
arr.length = write; // 截斷
注意:清楚寫入指標 write,避免誤刪。
模式 D:索引步幅(Step / Sampling)
情境:每隔 N 筆處理一次for (let i = 0; i < arr.length; i += 5) {
process(arr[i]);
}
模式 E:扁平化與轉換
const flat = nested.reduce((acc, cur) => acc.concat(cur), []); // or const flat2 = nested.flat(); // ES2019+
效能與可讀性:取捨指南
讀取次數:
將 arr.length 緩存到區域變數在現代引擎意義不大,但在熱點(hot path)仍可衡量。
陣列方法 vs for:
高階方法可讀性好;極限效能或需早停時,for/for...of 更直觀。
避免多次建立中間陣列:
鏈式操作多次 map/filter 會建立多個新陣列;可用一次 reduce 或組合邏輯。
資料量很大時:
考慮分批處理(批次)、Web Worker、串流處理(Node.js stream)等策略。
常見錯誤與雷點清單
1. forEach + await 不會等待
錯誤:
items.forEach(async (item) => {
await doAsync(item); // 不會被等待
});
正解(其一:逐項等待)
for (const item of items) {
await doAsync(item);
}
正解(其二:並行等待):
await Promise.all(items.map(doAsync));
2. 想早停卻用 forEach
錯誤:
items.forEach(item => {
if (shouldStop(item)) return; // 只退出 callback,不是整個迴圈
});
正解:
for (const item of items) {
if (shouldStop(item)) break;
}
3. for...in 遍歷陣列
錯誤:
for (const i in arr) {
// i 是「字串鍵」,且可能包含自訂屬性
}
正解:
for (const v of arr) { /* ... */ } // 值
// 或
for (let i = 0; i < arr.length; i++) { /* ... */ } // 索引
4. 漏掉邊界條件(<= / < 搞混)
錯誤:
for (let i = 0; i <= arr.length; i++) { /* arr[i] 可能 undefined */ }
正解:
for (let i = 0; i < arr.length; i++) { /* ... */ }
5. 無窮迴圈
錯誤:
let x = 0;
while (x < 10) {
// 忘了 x++ 或更新條件
}
正解:
let x = 0;
while (x < 10) {
// ...
x++;
}
6. 閉包 + var 變數提升
錯誤:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3,3,3
}
正解(用 let):
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0,1,2
}
備選(IIFE 綁定):
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 0))(i);
}
7. 迴圈內修改迭代目標(邊遍歷邊 splice)
錯誤:
for (let i = 0; i < arr.length; i++) {
if (bad(arr[i])) arr.splice(i, 1); // 長度變了,i 也亂了
}
正解(倒序刪除):
for (let i = arr.length - 1; i >= 0; i--) {
if (bad(arr[i])) arr.splice(i, 1);
}
或使用不可變策略:
const cleaned = arr.filter(x => !bad(x));
8. 浮點步進誤差
錯誤:
for (let x = 0; x !== 1; x += 0.1) { /* 可能永遠到不了 1 */ }
正解:
for (let i = 0; i <= 10; i++) {
const x = i / 10;
}
9. 物件遍歷漏掉 hasOwn
錯誤:
for (const k in obj) {
console.log(k, obj[k]);
}
正解:
for (const [k, v] of Object.entries(obj)) {
console.log(k, v);
}
10. reduce 初始值遺漏
錯誤:
[1,2,3].reduce((a, x) => a + x); // 初值缺失,空陣列會拋錯
正解:
[1,2,3].reduce((a, x) => a + x, 0);
從零到一:逐步操作範例
範例 1:將分數陣列轉成等第(A/B/C)
需求:[95, 81, 72, 66] → ["A","B","C","C"]
步驟
1. 定義轉換規則函式。
2. 使用 map 映射成新陣列。
function toGrade(score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
return "D";
}
const scores = [95, 81, 72, 66];
const grades = scores.map(toGrade);
範例 2:找出第一個可用座位(早停)
需求:陣列中第一個 available === true 的座位
const seat = seats.find(s => s.available === true);
// 或需紀錄索引
const idx = seats.findIndex(s => s.available);
if (idx !== -1) { /* 使用 seats[idx] */ }
範例 3:批次呼叫 API(每 5 筆一批)
async function fetchInBatches(ids, size = 5) {
const results = [];
for (let i = 0; i < ids.length; i += size) {
const batch = ids.slice(i, i + size).map(id => fetchOne(id));
results.push(...await Promise.all(batch));
}
return results;
}
範例 4:原地移除不合法的輸入(倒序刪除)
for (let i = inputs.length - 1; i >= 0; i--) {
if (!isValid(inputs[i])) inputs.splice(i, 1);
}
範例 5:物件轉陣列再統計
const priceMap = { apple: 25, banana: 10, cherry: 40 };
const total = Object.values(priceMap).reduce((a, x) => a + x, 0); // 75
測試與除錯建議
小步前進:先在 3~5 筆資料上跑通,再放大資料量。
加入不良案例:空陣列、單筆、極大值、重複值。
記錄與觀測:必要時在迴圈內加計數器或防護條件(上限次數)。
可視化:把迴圈中間結果(如 i、累加值)打印出來檢查。
風格與可維護性
命名即文件:items、total, index 讓意圖一眼可見。
邏輯分層:把複雜條件抽出成純函式,迴圈只負責「走訪」。
避免深巢狀:及早 continue / break 簡化縮排層級。
一致性:團隊約定「預設 for...of」或「能用 map/filter 就用」,降低心智負擔。
速查表
| 目的 | 推薦寫法 | 為何 |
|---|---|---|
| 需要索引、可早停 | for 索引或 for...of |
可 break/continue、掌控度高 |
| 可讀性優先的轉換/過濾 |
map / filter /
reduce
|
語意清晰、鏈式撰寫 |
| 逐項等待非同步 | for...of + await |
順序執行、確保完成 |
| 並行多請求 | Promise.all(items.map(...)) |
加速、簡潔 |
| 遍歷物件 |
Object.entries(obj) +
for...of
|
乾淨、無原型鏈干擾 |
| 原地刪除 | 倒序 for + splice |
不破壞索引 |
| 需要早停 |
for / for...of /
find
|
支援 break 或直接找到即停 |
| 要避免 |
forEach + await、陣列用
for...in
|
行為與預期不符、易出錯 |
附錄:常見片段
早停搜尋(回傳索引)
function findIndexById(items, id) {
for (let i = 0; i < items.length; i++) {
if (items[i].id === id) return i;
}
return -1;
}
去重與計數(Set + Map)
const unique = [...new Set(arr)]; const counter = new Map(); for (const x of arr) counter.set(x, (counter.get(x) ?? 0) + 1);
防無窮迴圈(安全上限)
let step = 0, limit = 1e6;
while (hasNext() && step < limit) {
consume();
step++;
}
if (step >= limit) throw new Error("Loop limit exceeded");
批次執行(API 速率友善)
async function throttleRun(items, size = 3, gapMs = 200) {
for (let i = 0; i < items.length; i += size) {
const batch = items.slice(i, i + size).map(doAsync);
await Promise.all(batch);
if (i + size < items.length) await new Promise(r => setTimeout(r, gapMs));
}
}
