在 JavaScript 編程中,for...of
和 for...in
是常用的循環語法,但它們在使用時可能會引發一些意想不到的問題。本文將分享我在使用這兩種循環時所遇到的坑和經驗。
兩者的區別:
- 適用對象類型:
-
- for…of:主要用于遍歷可迭代對象(例如數組、字符串、Set、Map等),可以獲取到迭代對象的值。
- for…in:主要用于遍歷對象的屬性(包括原型鏈上的屬性),可以獲取到屬性名(鍵)。
- 遍歷順序:
-
- for…of:按照對象的順序迭代,一般用于遍歷有序集合。
- for…in:無法保證屬性的遍歷順序,可能會導致屬性的無序輸出。
- 迭代內容:
-
- for…of:迭代的是對象的值本身,例如數組中的元素、字符串中的字符等。
- for…in:迭代的是對象的屬性名,需要通過屬性名訪問屬性值。
- 支持情況:
-
- for…of:在 ES6 中引入,適用于可迭代對象,如數組、字符串等。
- for…in:在早期版本的 JavaScript 中就存在,用于遍歷對象的屬性。但是不適用于數組等可迭代對象,因為它會遍歷出額外的屬性。
- 性能:
-
- for…of:通常性能比 for…in 更好,因為它不需要遍歷原型鏈上的屬性。
示例代碼演示兩者的不同用法:
// for...of 遍歷數組
const arr = [1, 2, 3, 4];
for (const element of arr) {console.log(element); // 輸出數組的每個元素
}// for...in 遍歷對象的屬性
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {console.log(key); // 輸出屬性名 a, b, cconsole.log(obj[key]); // 輸出屬性值 1, 2, 3
}// for...of 遍歷數組
const arr = [1, 2, 3, 4];
for (const element of arr) {console.log(element); // 輸出數組的每個元素
}// for...in 遍歷對象的屬性
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {console.log(key); // 輸出屬性名 a, b, cconsole.log(obj[key]); // 輸出屬性值 1, 2, 3
}
總之,如果你想遍歷數組或其他可迭代對象的值,使用 for…of;如果你想遍歷對象的屬性,使用 for…in。
for…of 遍歷數組的陷阱:
當使用 for...of
循環來遍歷數組時,我們通常是為了遍歷數組的元素,而不是索引。然而,在使用 for...of
循環時,有一些常見陷阱需要避免,特別是關于循環索引和遍歷順序的問題。下面是如何正確使用 for...of
循環來遍歷數組,以及如何避免這些陷阱的解釋:
1. 遍歷元素而非索引: 使用 for...of
循環時,我們直接遍歷數組的元素,而不需要關心索引的細節。這樣可以使代碼更加簡潔易讀。例如:
const array = [1, 2, 3, 4, 5];
for (const element of array) {console.log(element); // 輸出數組的每個元素
}
2. 避免使用索引: 避免在 for...of
循環中使用額外的索引變量,因為 for...of
循環本身已經直接提供了數組的每個元素。這有助于減少代碼復雜性和錯誤的機會。不推薦的寫法:
const array = [1, 2, 3, 4, 5];
for (let i = 0; i < array.length; i++) {console.log(array[i]); // 避免這種額外使用索引的方式
}
3. 保持遍歷順序: for...of
循環保證按照數組中的順序進行遍歷,因此它適用于需要按順序處理元素的場景。這確保了元素的處理順序與它們在數組中的位置一致。
4. 不會遍歷稀疏元素: for...of
循環不會遍歷數組中的稀疏元素(未賦值的元素),只會遍歷有實際值的元素。這有助于避免不必要的處理。
5. 適用于可迭代對象: 除了數組,for...of
循環還適用于其他可迭代對象,如字符串、Set、Map 等。這使得代碼具有更廣泛的適用性。
綜上所述,使用 for...of
循環來遍歷數組是一種更直觀、簡潔的方式,可以避免許多在傳統 for
循環中容易犯的錯誤。通過專注于元素而非索引,保持遍歷順序,并充分利用循環的簡潔性,我們可以提高代碼的可讀性和可維護性,減少錯誤的風險。
for…in 遍歷對象的不可靠性:
在使用 for...in
循環時,可能會遇到一些問題,其中包括遍歷順序的不確定性和遍歷到原型屬性的風險。下面是對這些問題的探討:
1. 遍歷順序的不確定性: for...in
循環無法保證遍歷對象屬性的順序。這是因為對象屬性在 ECMAScript 規范中被定義為無序的。因此,使用 for...in
循環來依賴屬性遍歷的特定順序是不可靠的。
2. 遍歷到原型屬性的風險: for...in
循環會遍歷對象自身屬性以及繼承自原型鏈的屬性。這可能會導致意外的屬性遍歷,尤其是當我們只想遍歷對象自身的屬性時。
3. 原型屬性被遍歷:
function Person() {this.name = 'Alice';
}Person.prototype.age = 30;const person = new Person();for (const prop in person) {console.log(prop); // 輸出 'name' 和 'age'
}
或
/*** @param {Function} fn* @return {Array}*/
array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
fn = function (n) {return String(n > 5);
}Array.prototype.groupBy = function (fn) {const myMap = new Map();const keys = Object.keys(this);console.log(keys);for (const key in keys) {if (myMap.has(fn(this[key]))) {myMap.set(fn(this[key]), myMap.get(fn(this[key])).concat([this[key]]));} else {myMap.set(fn(this[key]), [this[key]]);}}const myObj = {};for (const [key, value] of myMap) {myObj[key] = value;}return myObj;
};array.groupBy(fn);
/*** [1,2,3].groupBy(String) // {"1":[1],"2":[2],"3":[3]}*/
4. 使用 hasOwnProperty 過濾原型屬性:
for (const prop in person) {if (person.hasOwnProperty(prop)) {console.log(prop); // 僅輸出 'name'}
}
5. 遍歷可枚舉屬性:
Object.defineProperty(person, 'country', {value: 'USA',enumerable: true
});for (const prop in person) {console.log(prop); // 輸出 'name'、'age' 和 'country'
}
6. 遍歷不可枚舉屬性:
Object.defineProperty(person, 'address', {value: '123 Main St',enumerable: false
});for (const prop in person) {console.log(prop); // 僅輸出 'name' 和 'age'
}
7. 遍歷順序問題:
const obj = { a: 1, b: 2, c: 3 };for (const prop in obj) {console.log(prop); // 輸出 'a'、'b' 和 'c',但順序不確定
}
8. 遍歷字符串屬性:
const str = 'Hello';for (const char in str) {console.log(char); // 輸出 '0'、'1'、'2'、'3' 和 '4'
}
綜上所述,盡管 for...in
循環在某些情況下可以派上用場,但要特別小心遍歷順序的不確定性和遍歷到原型屬性的風險。在需要遍歷對象屬性時,推薦使用 Object.keys
、Object.values
或 Object.entries
等方法,以獲得更可靠的遍歷結果。或者,考慮使用 for...of
循環來遍歷數組和可迭代對象,以避免這些問題。