Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке symbol.iterator і навіщо він потрібен». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`Symbol.iterator`** - вбудований символ, який визначає, як JavaScript ітерує об'єкт. `for...of`, spread і `Array.from()` викликають `obj[Symbol.iterator]()`, щоб отримувати значення по одному. ```javascript const range = { [Symbol.iterator]() { let i = 1; return { next: () => i <= 3 ? { value: i++, done: false } : { done: true } }; } }; for (const n of range) console.log(n); // 1, 2, 3 console.log([...range]); // [1, 2, 3] ``` **Ключове:** будь-який об'єкт з методом `[Symbol.iterator]()` стає сумісним з `for...of`, spread і деструктуризацією.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`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 ### Швидкий приклад ```javascript // Звичайний об'єкт - немає 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 ітератор ```javascript 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()`. Ніякого попереднього завантаження, ніякого масиву в пам'яті. ### Типові помилки **Повторне використання вичерпаного ітератора:** ```javascript 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` вже є ітератором. Вичерпаний - означає вичерпаний. Виправлення: ітеруй оригінальний об'єкт, а не збережений ітератор. **Мутація джерела під час ітерації:** ```javascript 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-генератору:** ```javascript 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:** ```javascript 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() ```javascript // Явна реалізація 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-генератором ```javascript // Кожен 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` обробляє це автоматично. ### Обхід дерева як ітерабельний клас ```javascript 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) - воно дозволяє складати обходи дерев без ручного керування стеком.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.