For...in проти for...of в JavaScript
for...in перебирає перелічувані ключі властивостей (enumerable property keys) об'єкта у вигляді рядків; for...of перебирає значення з будь-якого ітерованого об'єкта: масиву, рядка, Map або Set.
Теорія
TL;DR
for...inчитає підписи на поштових скриньках (ключі);for...ofчитає самі листи (значення)for...inзавжди дає рядкові ключі;for...ofдає реальні значення будь-якого типуfor...inпроходить по ланцюгу прототипів (prototype chain);for...ofні- Простий об'єкт?
for...in. Масив, рядок, Map, Set?for...of for...inна масивах технічно запускається, але призводить до реальних багів
Швидкий приклад
const obj = { a: 1, b: 2 };
const arr = [1, 2];
for (const key in obj) {
console.log(key); // "a", "b" (рядкові ключі)
}
for (const val of arr) {
console.log(val); // 1, 2 (самі значення)
}for...in дає ключі у вигляді рядків. for...of дає значення напряму. Ось і вся різниця.
Ключова відмінність
for...in створений для об'єктів. Він проходить по всіх перелічуваних властивостях, включно зі спадкованими через ланцюг прототипів, і завжди повертає ключ як рядок. for...of з'явився в ES2015 і працює з будь-чим, що реалізує протокол ітератора (iterator protocol), тобто має метод [Symbol.iterator]. Жодного обходу прототипів, жодного приведення типів. Просто значення по порядку.
Коли що використовувати
- Перевіряєш структуру об'єкта (конфіг,
req.headers):for...in - Перебираєш елементи масиву, символи рядка або записи з Map/Set:
for...of - Потрібні і ключі, і значення об'єкта:
for (const [k, v] of Object.entries(obj)) - Хочеш уникнути проблем з прототипами:
Object.keys(obj)зfor...ofбезпечніше
Таблиця порівняння
| Аспект | for...in | for...of |
|---|---|---|
| Перебирає | Перелічувані ключі (рядки) | Значення з ітерованих об'єктів |
| Працює з | Об'єктами (включно з масивами) | Масивами, рядками, Map, Set, генераторами |
| Ланцюг прототипів | Включає спадковані властивості | Ігнорується |
| Поведінка з масивами | Індекси як рядки ("0", "1") | Прямі елементи, holes дають undefined |
| Типовий сценарій | Інспекція ключів об'єкта | Обхід колекцій |
Як це працює
for...in викликає внутрішню операцію enumerate, обходить власні і прототипні властивості і повертає кожен ключ як рядок. Тому на масивах ти отримуєш "0" замість 0.
for...of викликає [Symbol.iterator]() на цільовому об'єкті, потім послідовно викликає .next() і читає об'єкти {value, done} поки done не стане true. Жодного обходу прототипів. Якщо у об'єкта немає [Symbol.iterator], одразу отримаєш TypeError.
Типові помилки
for...in на масивах
const arr = ["a", "b"];
for (const i in arr) {
console.log(i, typeof i); // "0" "string", "1" "string"
}Індекси стають рядками, а не числами. Розріджені (sparse) масиви поводяться непередбачувано, а якщо якась бібліотека додала властивість до Array.prototype, вона теж потрапить у цикл. Використовуй for...of або звичайний for.
for...of на звичайному об'єкті
const obj = { a: 1, b: 2 };
for (const val of obj) { } // TypeError: obj is not iterableЗвичайні об'єкти не мають [Symbol.iterator]. Виправлення: for (const val of Object.values(obj)).
Забруднення прототипу і for...in
const arr = ["a", "b"];
Array.prototype.foo = "polluted";
for (const key in arr) {
console.log(key); // "0", "1", "foo"
}В старіших кодових базах з legacy-бібліотеками це зустрічається частіше ніж хочеться. Додавай перевірку hasOwnProperty або взагалі не використовуй for...in на масивах.
Зміна об'єкта під час ітерації
const obj = { a: 1, b: 2 };
for (const k in obj) {
delete obj.b; // Може пропустити "b" у V8 - поведінка не гарантована
}V8 робить знімок ключів до початку циклу. Видалення властивостей під час ітерації може призвести до пропусків. Збери ключі заздалегідь через Object.keys().
Де зустрічається в реальних проектах
- Express:
for...inпоreq.headersдля логування всіх заголовків запиту - React:
for...ofпри ручному обході масиву пропсів або побудові DOM-елементів - Node.js:
for...ofпоprocess.argv.slice(2)для обробки аргументів CLI - Lodash:
_.forInобгортаєfor...inбезпечно для роботи з об'єктами
Питання на співбесіді
Q: Що for...in повертає на масиві з "дірками" (holes)?
A: Пропускає їх, бо відповідної властивості просто немає. for...of дасть undefined на місці кожної дірки.
Q: Як зробити звичайний об'єкт ітерованим для for...of?
A: Оберни через Object.values(obj) або додай кастомний метод [Symbol.iterator] як генератор.
Q: Чому for...in повертає рядкові ключі на масивах?
A: Специфікація приводить всі ключі властивостей до рядків. Масиви зберігають елементи як іменовані властивості, тому індекс 0 стає "0".
Q: Порядок ітерації гарантований?
A: for...of на масивах і рядках дотримується порядку вставки. for...in порядок специфікацією не гарантований, хоча V8 зазвичай повертає власні ключі першими, потім прототипні.
Q: (Для досвідчених) Що відбувається у V8, якщо видалити властивість під час for...in?
A: V8 будує знімок ключів до початку циклу. Видалення після цього моменту не завжди виключає ключ з ітерації. Поведінка не визначена специфікацією, не покладайся на неї.
Приклади
Перевірка властивостей конфіг-об'єкта
const settings = { theme: "dark", lang: "en", debug: true };
for (const key in settings) {
console.log(`${key}: ${settings[key]}`);
}
// theme: dark
// lang: en
// debug: truefor...in доречний для простих об'єктів, коли потрібні ключі. Такий патерн зустрічається у валідації конфігів, логувальних middleware і всюди, де треба пройтись по об'єкту невідомої форми.
Побудова списку з масиву
const todos = ["Купити молоко", "Написати код", "Поспати"];
const list = document.createElement("ul");
for (const todo of todos) {
const item = document.createElement("li");
item.textContent = todo;
list.appendChild(item);
}
// <ul><li>Купити молоко</li><li>Написати код</li><li>Поспати</li></ul>for...of дає елементи напряму, без індексних змінних і без arr[i]. Це стандартний патерн для побудови DOM-елементів у циклі або обробки будь-якої впорядкованої колекції.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.