Skip to main content

Що таке генератори в JavaScript?

Генератори (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-результати як синхронні присвоювання.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?