Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке генератори в JavaScript?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Генератори** (generators) - це функції JavaScript, що зупиняють виконання на `yield` і відновлюють його при виклику `.next()`. ```javascript function* counter() { yield 1; yield 2; yield 3; } const gen = counter(); gen.next(); // { value: 1, done: false } gen.next(); // { value: 2, done: false } ``` **Головне:** значення виробляються ліниво, по одному на кожен `.next()`, не всі відразу.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Генератори** (generators) - це функції JavaScript, що призупиняють виконання посередині і відновлюють його точно з тієї точки, де зупинились, виробляючи значення по одному через ключове слово `yield`. ## Теорія ### TL;DR - Як вендинговий автомат: кидаєш монету (`.next()`), отримуєш одну позицію (`yield` значення), і автомат чекає наступну монету замість того щоб видати все відразу. - Звичайна функція виконується повністю і повертає одне значення. Генератор повертає об'єкт-ітератор миттєво і виробляє значення ліниво, по одному на кожен `.next()`. - Використовуй генератори при обробці великих наборів даних або нескінченних послідовностей, де завантажувати все в пам'ять одразу не виходить (зазвичай від 10k+ елементів). - `function*` і `yield` - не синтаксичний цукор. Це те, що робить функцію генератором. ### Швидкий приклад ```javascript function* numberGen() { yield 1; // зупиняється тут на першому .next() yield 2; yield 3; return 4; // фінальне значення, потім done: true } const gen = numberGen(); console.log(gen.next()); // { value: 1, done: false } console.log(gen.next()); // { value: 2, done: false } console.log(gen.next()); // { value: 3, done: false } console.log(gen.next()); // { value: 4, done: true } ``` Кожен `.next()` відновлює виконання з останнього `yield`, повертає `{ value, done }` і знову зупиняється. Після `return` кожен наступний `.next()` повертатиме `{ value: undefined, done: true }`. ### Головна різниця від звичайних функцій Звичайна функція запускається до кінця в момент виклику і повертає одне значення. Генератор при виклику повертає об'єкт-ітератор миттєво, без запуску тіла. Тіло починає виконуватись тільки на першому `.next()`, потім зупиняється на кожному `yield`. Це ліниві обчислення (lazy evaluation): значення обчислюються тільки коли їх запросять, а не наперед. ### Коли використовувати генератори - **Великі або нескінченні дані**: генерація пагінованих результатів API, числових діапазонів або обходу дерева по запиту, замість того щоб будувати повний масив заздалегідь. - **Стейт-машини**: відстеження багатокрокових процесів на зразок ходів у грі або multi-step форми без класу. - **Двостороння комунікація**: передача значень назад у запущений генератор через `.next(value)`, активно використовується в Redux-Saga. - Для простих одноразових обчислень генератори зайві. Там звичайна функція і є правильним вибором. ### Як V8 компілює генератори V8 компілює `function*` у стейт-машину. Кожен `yield` стає точкою переходу між станами. Об'єкт генератора тримає вказівник на призупинений контекст виконання. Коли викликаєш `.next(arg)`, рушій відновлює роботу з того стану, а `arg` стає значенням, що повертає вираз `yield` всередині функції. Саме тому можна не тільки отримувати значення назовні, але й передавати їх всередину. ### Типові помилки **Очікування що генератор запуститься при створенні** ```javascript function* gen() { yield 1; } const g = gen(); // g - це ітератор, НЕ значення 1 console.log(g); // Generator {} ``` Жоден код не виконується до першого `.next()`. Створення генератора і його запуск - це два окремі кроки. **Пропуск перевірки `done: true`** ```javascript function* gen() { yield 1; } const g = gen(); g.next(); // { value: 1, done: false } g.next(); // { value: undefined, done: true } console.log(g.next().value); // undefined - генератор вичерпано ``` Після того як генератор завершив роботу, всі наступні `.next()` повертають `{ value: undefined, done: true }`. Захистись через цикл `while`: ```javascript let result = g.next(); while (!result.done) { console.log(result.value); result = g.next(); } ``` **Випадкове спільне використання стану між екземплярами** ```javascript let count = 0; function* sharedGen() { yield count++; } const g1 = sharedGen(); g1.next(); // { value: 0, done: false } const g2 = sharedGen(); g2.next(); // { value: 1, done: false } - спільний зовнішній count! ``` Кожен виклик ділить замикання (closure) на зовнішній scope. Перенеси стан всередину щоб отримати незалежні генератори: ```javascript function* gen() { let count = 0; yield count++; } ``` **Нерозуміння ін'єкції значень на першому `.next()`** Будь-який аргумент, переданий у перший `.next()`, ігнорується. Всередині функції ще немає жодного `yield`-виразу що чекає на отримання значення. Починаючи з другого виклику, аргумент `.next(arg)` стає значенням, яке повертає попередній `yield`-вираз. Повний приклад є у розділі прикладів нижче. ### Де зустрічається в реальному коді - **Redux-Saga**: ефекти `call()` і `takeEvery()` побудовані на призупиненні генераторів. Saga middleware викликає `.next()` з розвʼязаними значеннями для координації async-операцій. - **Node.js streams**: `readableStream[Symbol.asyncIterator]()` використовує async generators для обробки backpressure (Node 10+). - **Нескінченні послідовності**: генератори ID, числові діапазони або фабрики тестових даних, де не знаєш наперед скільки значень знадобиться. - **Пагіновані запити до БД**: вибірка батчами з yielding кожної сторінки без завантаження всього датасету в пам'ять. Я використовував цей підхід у Node.js-сервісі що обробляв 500k+ записів користувачів. Завантаження всіх одразу давало стрибки пам'яті. Генератор з батчами по 500 тримав споживання рівним протягом усього запуску. ### Питання на співбесіді **Q:** Як ітерувати по генератору? **A:** Три способи: `for...of` (автоматично викликає `.next()` до `done: true`), spread-оператор (`[...gen]` конвертує в масив) або ручний цикл `while (!result.done)`. Використовуй `for...of` якщо не треба збирати результат в масив. **Q:** Що таке `yield*` і чим відрізняється від `yield`? **A:** `yield` зупиняється з одним значенням. `yield*` делегує до іншого ітерабельного об'єкта або генератора і послідовно виробляє всі його значення. По суті це скорочення для циклу що робить `yield` кожного елемента вкладеного ітерабельного. **Q:** Чи може генератор обробляти помилки? **A:** Так. Виклич `.throw(err)` щоб ін'єктувати помилку в поточну точку `yield`. Якщо генератор обгортає цей `yield` в `try/catch`, він обробляє помилку і продовжує роботу. Якщо ні, помилка пробрасується до колера. **Q:** Яка перевага генераторів перед масивами по пам'яті для великих послідовностей? **A:** Масив з мільйоном елементів виділяє пам'ять для всіх одразу. Генератор виробляє одне значення на `.next()`, повторно використовуючи той самий контекст виконання без додаткового виділення стеку. Для нескінченних послідовностей масив взагалі не варіант. **Q:** Як генератори пов'язані з async iteration (рівень сеніора)? **A:** `async function*` поєднує генератори з промісами. Кожен `yield` може чекати на async-операцію перед тим як виробити значення. Колер ітерує через `for await...of`. V8 обробляє це через призупинення на рівні bytecode, аналогічно звичайним генераторам, але з інтеграцією розвʼязання промісів. ## Приклади ### Базовий: генератор діапазону з `for...of` Найпростіший реальний приклад: генерація числового діапазону без побудови масиву. ```javascript function* range(start, end) { for (let i = start; i <= end; i++) { yield i; // зупиняється після кожного значення } } for (const num of range(1, 5)) { console.log(num); // 1, 2, 3, 4, 5 } // for...of сам обробляє .next() і перевірку done ``` `for...of` викликає `.next()` всередині і зупиняється коли `done: true`. Писати цикл вручну не потрібно. ### Проміжний: пагінований запит до бази даних Вибірка користувачів по сторінкам без завантаження всіх записів одразу. ```javascript function* fetchUsersPaginated(pageSize = 5) { let page = 0; while (true) { const users = fetchUsersFromDB(page, pageSize); // умовний виклик до БД if (users.length === 0) return; // зупиняємось коли даних більше немає yield users; // одна сторінка за раз page++; } } const userGen = fetchUsersPaginated(); console.log(userGen.next().value); // [user1, user2, user3, user4, user5] console.log(userGen.next().value); // [user6, user7, user8, user9, user10] // в пам'яті тільки поточна сторінка ``` Генератор зупиняється після кожного `yield users` і завантажує наступну сторінку тільки при наступному `.next()`. Споживання пам'яті залишається передбачуваним незалежно від загальної кількості записів. ### Просунутий: двостороння комунікація через `.next(value)` Паттерн що спантеличує більшість розробників. Значення течуть як назовні (через `yield`) так і всередину (через `.next(arg)`). ```javascript function* auctionGen() { const bid = yield 'Waiting for first bid'; // зупиняється; 'bid' отримає аргумент наступного .next() const counterBid = yield `Current high: ${bid}`; // зупиняється знову yield `Sold for ${Math.max(bid, counterBid)}`; } const auction = auctionGen(); console.log(auction.next()); // { value: 'Waiting for first bid', done: false } console.log(auction.next(100)); // 100 стає 'bid' → { value: 'Current high: 100', done: false } console.log(auction.next(150)); // 150 стає 'counterBid' → { value: 'Sold for 150', done: false } ``` Redux-Saga використовує саме цей механізм. Saga middleware передає розвʼязані значення промісів назад у генератор через `.next()`, дозволяючи сагам читати async-результати як синхронні присвоювання.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.