本文 AI 產出,尚未審核

JavaScript 控制流程:深入了解 for...in 與 for...of


簡介

在 JavaScript 中,**迴圈(loop)**是處理集合資料、重複執行相同程式碼的核心工具。除了最常見的 for、while,ES6 之後引入的 for...in 與 for...of 兩種語法,讓我們在遍歷物件與可迭代資料(iterable)時更加直觀且安全。

  • for...in 主要用於 列舉(enumerate)物件的屬性名稱,適合處理純粹的 key‑value 結構,如普通物件或 Map 的鍵集合。
  • for...of 則是 遍歷可迭代物件的值(value),支援陣列、字串、Set、Map、arguments 甚至自訂的 iterable。

了解這兩者的差異與正確使用時機,能讓程式碼更具可讀性、效能更佳,同時避免常見的錯誤陷阱。以下,我們將從概念說明、實作範例、最佳實踐一路帶你深入掌握。


核心概念

1. for...in:列舉物件屬性名稱

for...in 會遍歷 物件本身以及原型鏈上 可列舉(enumerable)的屬性名稱(key),回傳的是字串形式的屬性名。

const person = {
  name: 'Alice',
  age: 28,
  hobby: 'photography'
};

for (const key in person) {
  console.log(key);           // => "name", "age", "hobby"
}

為什麼會遍歷原型鏈?

for...in 的設計初衷是 列舉所有可見屬性,包括從原型繼承的屬性。若只想遍歷「自有屬性」(own properties),必須搭配 hasOwnProperty 檢查:

for (const key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key, person[key]);
  }
}

小技巧:在 ES5+ 中,Object.keys()、Object.values()、Object.entries() 都可以直接取得自有可列舉屬性,往往比 for...in 更安全。


2. for...of:遍歷可迭代物件的值

for...of 只關心 可迭代(iterable) 的資料結構,透過內建的 @@iterator(Symbol.iterator)取得每一次的值。它不會遍歷原型鏈,也不會返回索引或鍵。

const colors = ['red', 'green', 'blue'];

for (const color of colors) {
  console.log(color);   // => "red", "green", "blue"
}

支援的資料類型

類型 說明
Array 常見的陣列遍歷
String 逐字元遍歷(包括 Unicode 代理對)
Set 只會返回唯一值
Map 依序返回 [key, value] 兩元組
arguments 函式內的類陣列物件
自訂 iterable 必須實作 Symbol.iterator 方法
// 文字逐字元遍歷(支援 emoji)
const emoji = '👍🏽💖';
for (const ch of emoji) {
  console.log(ch);   // => "👍🏽", "💖"
}

3. for...in vs for...of:何時該選哪一個?

條件 使用 for...in 使用 for...of
想取得 屬性名稱(key) ✅ ❌
只遍歷 自有屬性(不含原型) ❌(需 hasOwnProperty) ✅(適用於 iterable)
需要 值的順序(如陣列索引) ❌ ✅
處理 字串、Set、Map ❌ ✅
需要 兼容舊瀏覽器(ES5 前) ✅ ❌(需 ES6)

程式碼範例

以下提供 5 個實用範例,示範在不同情境下如何正確使用 for...in 與 for...of。

範例 1:列舉物件屬性並過濾自有屬性

const car = {
  brand: 'Toyota',
  model: 'Corolla',
  year: 2020
};

Object.prototype.wheels = 4; // 假設有人在原型上加了屬性

for (const prop in car) {
  // 只處理自有屬性
  if (car.hasOwnProperty(prop)) {
    console.log(`${prop}: ${car[prop]}`);
  }
}
// 輸出:
// brand: Toyota
// model: Corolla
// year: 2020

註解:hasOwnProperty 可避免遍歷到 wheels 這類原型屬性。


範例 2:使用 Object.entries() 搭配 for...of 直接取得鍵值對

const settings = {
  theme: 'dark',
  language: 'zh-TW',
  notifications: true
};

for (const [key, value] of Object.entries(settings)) {
  console.log(`${key} => ${value}`);
}
// 輸出:
// theme => dark
// language => zh-TW
// notifications => true

說明:Object.entries() 會回傳一個陣列,裡面的每個子陣列都是 [key, value],配合 for...of 可一次取得兩者。


範例 3:遍歷 Map 並同時取得鍵和值

const scores = new Map([
  ['Alice', 95],
  ['Bob', 82],
  ['Charlie', 78]
]);

for (const [name, score] of scores) {
  console.log(`${name} 的成績是 ${score}`);
}
// 輸出:
// Alice 的成績是 95
// Bob 的成績是 82
// Charlie 的成績是 78

重點:Map 本身即為 iterable,for...of 直接返回 [key, value] 陣列。


範例 4:使用 for...of 逐字元處理 Unicode 字串

const text = 'Hello 🌍!';

for (const ch of text) {
  console.log(ch);
}
// 輸出:
// H e l l o   🌍 !
// 每個字元(包括 emoji)都正確被切分

為什麼 for...in 不適合?
for...in 會遍歷字串的索引(如 "0", "1", ...),而且無法正確處理代理對(surrogate pair)產生的 emoji。


範例 5:自訂 Iterable 物件(實作 Symbol.iterator)

class Counter {
  constructor(limit) {
    this.limit = limit;
  }

  // 必須回傳一個 iterator 物件
  [Symbol.iterator]() {
    let count = 0;
    const limit = this.limit;

    // iterator 必須有 next() 方法,回傳 { value, done }
    return {
      next() {
        if (count < limit) {
          return { value: count++, done: false };
        }
        return { done: true };
      }
    };
  }
}

const counter = new Counter(5);
for (const num of counter) {
  console.log(num); // 0 1 2 3 4
}

說明:只要物件實作 Symbol.iterator,就能使用 for...of 迭代,這讓自訂資料結構也能享有同樣的語法便利。


常見陷阱與最佳實踐

陷阱 說明 解決方案
遍歷陣列時使用 for...in for...in 會列舉索引(字串)且會遍歷原型鏈,導致順序不一定,且效能較差。 永遠使用 for...of 或傳統 for、Array.prototype.forEach。
忘記 hasOwnProperty 直接使用 for...in 會把原型上的屬性也列出,可能導致意外行為。 加上 if (obj.hasOwnProperty(key)),或改用 Object.keys()、Object.entries()。
在 for...of 中修改陣列長度 迭代過程中若增減元素,可能導致跳過或重複遍歷。 避免在迭代期間改變原始陣列,或先複製 (slice()) 再遍歷。
使用 for...of 迭代非 iterable 如普通物件、null、undefined 會拋出 TypeError。 先檢查 obj != null && typeof obj[Symbol.iterator] === 'function',或使用 Object.entries()。
忘記 break / continue for...of 和 for...in 都支援 break、continue,但在 for...of 中使用 return 只能跳出整個函式。 清楚瞭解控制流程,必要時使用標籤(label)或抽離迴圈的函式。

最佳實踐

  1. 優先使用 for...of:遍歷陣列、字串、Set、Map 時語意最清晰。
  2. 僅在需要屬性名稱時使用 for...in,且配合 hasOwnProperty 或 Object.keys()。
  3. 避免在迴圈內改變被迭代的資料結構,若必須變更,先複製再處理。
  4. 使用 const 宣告迭代變數,確保迴圈內不會意外重新指派。
  5. 在大型迭代時考慮效能:for...of 的底層實作相較於傳統 for 仍有微小差距,對於極限效能需求可自行測試。

實際應用場景

1. 表格資料渲染(陣列 → DOM)

const rows = [
  { id: 1, name: 'Alice', score: 92 },
  { id: 2, name: 'Bob',   score: 85 },
  { id: 3, name: 'Carol', score: 78 }
];

const tbody = document.querySelector('#result tbody');

for (const row of rows) {
  const tr = document.createElement('tr');
  tr.innerHTML = `
    <td>${row.id}</td>
    <td>${row.name}</td>
    <td>${row.score}</td>
  `;
  tbody.appendChild(tr);
}

使用 for...of 可直接取得每筆資料,寫出簡潔且易讀的渲染程式碼。

2. 解析 API 回傳的 JSON(物件屬性列舉)

async function fetchUserSettings() {
  const resp = await fetch('/api/settings');
  const data = await resp.json(); // 假設回傳物件

  for (const key in data) {
    if (data.hasOwnProperty(key)) {
      console.log(`設定 ${key} = ${data[key]}`);
    }
  }
}

若不確定回傳的屬性是否為自有屬性,使用 hasOwnProperty 保障安全。

3. 計算唯一字元出現次數(使用 Set + for...of)

function countUniqueChars(str) {
  const unique = new Set();
  for (const ch of str) {
    unique.add(ch);
  }
  return unique.size;
}

console.log(countUniqueChars('abcaab')); // 3

Set 天然去除重複,搭配 for...of 能簡潔完成任務。

4. 自訂資料結構的遍歷(例如樹狀結構)

class TreeNode {
  constructor(value, children = []) {
    this.value = value;
    this.children = children;
  }

  *[Symbol.iterator]() {
    yield this.value;
    for (const child of this.children) {
      yield* child; // 深度優先遍歷
    }
  }
}

const tree = new TreeNode(1, [
  new TreeNode(2, [new TreeNode(4), new TreeNode(5)]),
  new TreeNode(3)
]);

for (const val of tree) {
  console.log(val); // 1 2 4 5 3
}

透過自訂 Symbol.iterator,任何樹狀資料都能以 for...of 直觀遍歷。


總結

  • for...in 列舉物件的屬性名稱,適合需要鍵名的情境,但必須小心原型鏈與非自有屬性的問題。
  • for...of 遍歷可迭代資料的值,支援陣列、字串、Set、Map 以及自訂 iterable,語意最為直觀且不會受到原型鏈干擾。
  • 在開發中,優先考慮 for...of,除非真的需要鍵名或要處理舊版瀏覽器環境時才使用 for...in。
  • 配合 Object.keys()、Object.entries()、hasOwnProperty 等輔助方法,可讓 for...in 的使用更安全。
  • 了解兩者的差異與限制,能寫出 更易讀、效能更佳且不易出錯 的程式碼,無論是前端 UI 渲染、資料處理或是自訂資料結構,都能從容應對。

祝你在 JavaScript 的控制流程世界裡,玩得開心、寫得順手! 🎉