Що таке symbol.iterator і навіщо він потрібен
Symbol.iterator - вбудований символ у JavaScript, який визначає, як об'єкт генерує послідовність значень для for...of, spread (...), Array.from() і деструктуризації.
Теорія
TL;DR
Symbol.iterator- як слот для монет у вендинговому автоматі: визначає інтерфейс для отримання значень по одному- Масиви, рядки, Set і Map мають його вбудованим; звичайні об'єкти - ні
- Будь-який об'єкт з
[Symbol.iterator]()працює вfor...of, spread і деструктуризації - Метод повинен повертати ітератор (iterator): об'єкт з
next(), що повертає{ value, done } - Потрібний кастомний порядок або ліниве завантаження? Реалізуй. Інакше використовуй Array або Set
Швидкий приклад
// Звичайний об'єкт - немає Symbol.iterator, кидає TypeError
const obj = { a: 1, b: 2 };
for (const v of obj) console.log(v); // TypeError: obj is not iterable
// Додаємо Symbol.iterator - тепер працює скрізь
const iterable = {
data: [10, 20, 30],
*[Symbol.iterator]() { // генератор бере next() на себе
for (const v of this.data) yield v;
}
};
for (const v of iterable) console.log(v); // 10, 20, 30
console.log([...iterable]); // [10, 20, 30]Синтаксис генератора (function*) автоматично реалізує next(). Результат такий самий, як якби ти писав { next: () => ({ value, done }) } вручну.
Як працює протокол ітерації
for...of виконує три кроки. Один раз викликає obj[Symbol.iterator](), щоб отримати ітератор. Потім на кожному кроці циклу викликає iterator.next(). Зупиняється, коли next() повертає { done: true }.
Тут є дві окремі ролі. Ітерабельний (iterable) - об'єкт з методом [Symbol.iterator](). Ітератор (iterator) - те, що цей метод повертає: об'єкт зі станом і методом next(). Масиви є обома одночасно, бо arr[Symbol.iterator]() повертає сам масив з прикріпленим next().
Ця різниця важлива на практиці. Ітератор має стан і вичерпується після одного проходу. Ітерабельний об'єкт кожного разу створює свіжий ітератор.
Ітерабельний vs ітератор
const arr = [1, 2, 3];
const it = arr[Symbol.iterator](); // отримуємо свіжий ітератор
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }Коли реалізовувати Symbol.iterator
Реалізовуй, коли є структура даних, що має поводитись як послідовність: діапазон чисел, посторінковий результат запиту, обхід дерева або обгортка DOM-елементів. Якщо Array або Set вже підходять - немає сенсу додавати.
Практичний сигнал: якщо ти постійно пишеш методи getAll(), що повертають масиви, і всі споживачі одразу ітерують результат - [Symbol.iterator] безпосередньо на об'єкті виглядатиме чистіше.
Як рушій обробляє це
V8 перевіряє obj[Symbol.iterator] на початку for...of. Якщо властивість відсутня або не є функцією - кидає TypeError: obj is not iterable ще до того, як виконається тіло циклу. Далі в циклі викликає next(), зчитуючи value і done з кожного результату. Генератори компілюються у машини станів у байткоді V8: кожен yield зберігає позицію виконання і локальні змінні, а next() продовжує з того місця.
Лінивість ітераторів має практичне значення. Генератор, що підтягує дані зі сторінки API, не робить запит до мережі, поки не викличуть next(). Ніякого попереднього завантаження, ніякого масиву в пам'яті.
Типові помилки
Повторне використання вичерпаного ітератора:
const it = myIterable[Symbol.iterator](); // збережений ітератор, не ітерабельний
for (const v of it) console.log(v); // 1, 2, 3
for (const v of it) console.log(v); // нічого - вже вичерпанийfor...of на ітерабельному об'єкті щоразу викликає [Symbol.iterator]() заново. Але it вже є ітератором. Вичерпаний - означає вичерпаний. Виправлення: ітеруй оригінальний об'єкт, а не збережений ітератор.
Мутація джерела під час ітерації:
const obj = {
data: [1, 2, 3],
*[Symbol.iterator]() { yield* this.data; }
};
for (const v of obj) {
obj.data.shift(); // мутуємо живий масив у циклі
console.log(v); // виводить 1, 3 - пропускає 2
}Генератор читає з живого посилання. Якщо потрібна мутація - спочатку клонуй дані.
Синхронна ітерація по async-генератору:
async function* fetchItems() { yield await Promise.resolve(1); }
for (const v of fetchItems()) {} // TypeError
for await (const v of fetchItems()) {} // правильноfor...of очікує синхронний next(). Async-генератори потребують for await...of і використовують [Symbol.asyncIterator] всередині.
Нескінченний ітератор без break:
const counter = {
*[Symbol.iterator]() { let i = 0; while (true) yield i++; }
};
for (const n of counter) {
if (n > 10) break; // break викликає iterator.return() всередині
console.log(n);
}
// Без break - цикл ніколи не завершитьсяДе зустрічається
- Node.js streams:
Readable.from(iterable)(Node 12+) перетворює будь-який ітератор на readable stream Array.from: використовує[Symbol.iterator]всередині, так само як spread- Деструктуризація:
const [a, b] = myIterableтеж викликає[Symbol.iterator] Promise.all/Promise.race: обидва приймають будь-який ітерабельний об'єкт, не тільки масиви- Кастомні діапазони:
for (const i of range(1, 100))читається краще за звичайнийforз індексом
Найбільше я використовував [Symbol.iterator] на обгортках для курсорів баз даних: коли клієнт БД повертає cursor, додавання ітераторного протоколу дозволяє коду-споживачу залишатись чистим без жодних змін в інтерфейсі.
Follow-up питання
Q: Яка різниця між ітерабельним і ітератором?
A: Ітерабельний (iterable) має [Symbol.iterator](), який щоразу повертає свіжий ітератор. Ітератор (iterator) - це об'єкт зі станом і методом next(). Масиви є обома: arr[Symbol.iterator]() повертає сам масив з прикріпленим next().
Q: Як реалізувати скінченний range без генераторів?
A: const range = (end) => ({ [Symbol.iterator]() { let i = 0; return { next: () => i < end ? { value: i++, done: false } : { done: true } }; } });
Q: Чому for...of кидає помилку на звичайних об'єктах, а for...in працює?
A: for...in використовує внутрішній механізм перебору властивостей, окремий від протоколу ітерації. for...of суворо вимагає [Symbol.iterator]. Два різні механізми зі своїми специфікаціями.
Q: Що відбувається при break у for...of над генератором?
A: Рушій викликає iterator.return(), якщо він є, що запускає блок finally генератора. Це дозволяє генераторам звільняти ресурси при ранньому виході: закривати файлові дескриптори, відміняти запити.
Q: (Senior) Як V8 компілює генераторну функцію?
A: Як машину станів. Кожен yield розбиває тіло функції на пронумеровані стани. next() переходить до поточного стану через bytecode dispatch, виконує до наступного yield, зберігає локальні змінні і номер стану в замиканні GeneratorObject, потім призупиняється.
Приклади
Range-ітератор з ручним next()
// Явна реалізація next() - без генераторів
function range(start, end) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
};
}
for (const n of range(1, 5)) console.log(n); // 1 2 3 4 5
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
const [first, second] = range(10, 20);
console.log(first, second); // 10 11Всі три варіанти - for...of, spread і деструктуризація - викликають [Symbol.iterator]() всередині. Один і той самий об'єкт працює в усіх трьох контекстах без додаткового коду.
Посторінковий fetch з async-генератором
// Кожен yield - один user; пагінація прихована від споживача
async function* fetchAllUsers(baseUrl) {
let page = 1;
while (true) {
const res = await fetch(`${baseUrl}?page=${page++}&limit=10`);
const data = await res.json();
if (!data.length) return; // більше сторінок немає
yield* data; // стрімимо елементи сторінки по одному
}
}
// Обробляємо users без завантаження всіх сторінок
for await (const user of fetchAllUsers('/api/users')) {
console.log(user.name);
if (user.role === 'admin') break; // ранній вихід, зайвих запитів немає
}Цей патерн підходить для SSE-роутів в Express і data-шарів Next.js, де повне попереднє завантаження неприйнятне. Зверни увагу: async-генератори використовують Symbol.asyncIterator, а не Symbol.iterator. for await...of обробляє це автоматично.
Обхід дерева як ітерабельний клас
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
// Обхід у глибину через делегування генераторів
*[Symbol.iterator]() {
yield this.value;
for (const child of this.children) {
yield* child; // делегуємо Symbol.iterator дочірнього вузла
}
}
}
const tree = new TreeNode(1, [
new TreeNode(2, [new TreeNode(4), new TreeNode(5)]),
new TreeNode(3)
]);
console.log([...tree]); // [1, 2, 4, 5, 3]yield* делегує іншому ітерабельному об'єкту: викликає child[Symbol.iterator]() і вичерпує його до кінця перш ніж продовжити. Це делегування генераторів (generator delegation) - воно дозволяє складати обходи дерев без ручного керування стеком.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.