Що таке генератори в JavaScript?
Генератори (generators) - це функції JavaScript, що призупиняють виконання посередині і відновлюють його точно з тієї точки, де зупинились, виробляючи значення по одному через ключове слово yield.
Теорія
TL;DR
- Як вендинговий автомат: кидаєш монету (
.next()), отримуєш одну позицію (yieldзначення), і автомат чекає наступну монету замість того щоб видати все відразу. - Звичайна функція виконується повністю і повертає одне значення. Генератор повертає об'єкт-ітератор миттєво і виробляє значення ліниво, по одному на кожен
.next(). - Використовуй генератори при обробці великих наборів даних або нескінченних послідовностей, де завантажувати все в пам'ять одразу не виходить (зазвичай від 10k+ елементів).
function*іyield- не синтаксичний цукор. Це те, що робить функцію генератором.
Швидкий приклад
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 всередині функції. Саме тому можна не тільки отримувати значення назовні, але й передавати їх всередину.
Типові помилки
Очікування що генератор запуститься при створенні
function* gen() { yield 1; }
const g = gen(); // g - це ітератор, НЕ значення 1
console.log(g); // Generator {}Жоден код не виконується до першого .next(). Створення генератора і його запуск - це два окремі кроки.
Пропуск перевірки done: true
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:
let result = g.next();
while (!result.done) {
console.log(result.value);
result = g.next();
}Випадкове спільне використання стану між екземплярами
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. Перенеси стан всередину щоб отримати незалежні генератори:
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
Найпростіший реальний приклад: генерація числового діапазону без побудови масиву.
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() і перевірку donefor...of викликає .next() всередині і зупиняється коли done: true. Писати цикл вручну не потрібно.
Проміжний: пагінований запит до бази даних
Вибірка користувачів по сторінкам без завантаження всіх записів одразу.
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)).
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-результати як синхронні присвоювання.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.