javaScript : 迴圈處理

 


想像你在處理一份上千筆的訂單名單,要先篩選合格、再計算總額、最後批次打 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));
  }
}


延伸閱讀推薦:



張貼留言 (0)
較新的 較舊