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)或抽離迴圈的函式。 |
最佳實踐
- 優先使用
for...of:遍歷陣列、字串、Set、Map 時語意最清晰。 - 僅在需要屬性名稱時使用
for...in,且配合hasOwnProperty或Object.keys()。 - 避免在迴圈內改變被迭代的資料結構,若必須變更,先複製再處理。
- 使用
const宣告迭代變數,確保迴圈內不會意外重新指派。 - 在大型迭代時考慮效能:
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 的控制流程世界裡,玩得開心、寫得順手! 🎉