Функції-генератори
Функція-генератор (generator function) це функція, яка може призупинити своє виконання і потім продовжити його з того самого місця. Вона повертає об'єкт-генератор і віддає значення по одному через yield, тому на ній зручно будувати ітератори, ліниві обчислення та асинхронні цикли.
Теорія
TL;DR
- Оголошення має вигляд
function* name() {}: зірочка післяfunctionробить функцію генератором. - Виклик не запускає тіло, а повертає об'єкт-генератор (ітератор) з методом
next(). yieldвіддає значення і «заморожує» виконання; наступнийnext()продовжує з того самого рядка.- Генератор сумісний з протоколом ітератора, тому працює у
for...of, спреді та деструктуризації. next(value)передає значення назад усередину генератора, тож обмін двосторонній.async function*разом зfor await...ofдають те саме для асинхронних потоків (ES2018).
Швидкий приклад
function* gen() {
yield 1;
yield 2;
yield 3;
}
const iterator = gen();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }Що тут відбувається:
- перший виклик
next()доводить виконання до першогоyield, повертає1і «заморожує» функцію; - наступний
next()продовжує виконання з того самого місця; - після останнього
yieldгенератор завершує роботу і віддаєdone: true.
Синтаксис і об'єкт-генератор
Функція-генератор це функція, яка повертає об'єкт-генератор (ітератор), а всередині себе використовує ключове слово yield, щоб призупиняти виконання і повертати значення по одному.
function* name(args) {
// тіло функції
}*післяfunctionробить функцію генератором;yieldвикористовують для покрокової видачі значень;- під час виклику функція не виконується одразу, а повертає генератор (ітератор).
Кожен виклик next() повертає об'єкт виду { value, done }, де value це те, що віддав yield, а done показує, чи генератор уже завершився.
Генератори як ітератори
Генератор повертає об'єкт, сумісний з протоколом ітераторів. Це означає, що його можна напряму передати в for...of:
function* numbers() {
yield 10;
yield 20;
yield 30;
}
for (const n of numbers()) {
console.log(n);
}Вивід:
10
20
30Так само генератор працює зі спред-оператором ([...numbers()]) і з деструктуризацією масиву.
Ліниві обчислення і нескінченні послідовності
Генератори можуть бути нескінченними: вони не створюють весь масив одразу, а видають значення «на запит». Це і є ліниві обчислення, значення генеруються в міру потреби.
function* infiniteCounter() {
let i = 1;
while (true) {
yield i++;
}
}
const counter = infiniteCounter();
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3
// ...і так нескінченноТой самий підхід зручний для скінченних послідовностей, які не варто матеріалізувати в масив:
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
for (const num of range(1, 5)) {
console.log(num);
}Вивід:
1
2
3
4
5Двосторонній обмін: next(value) і return
Дані можна передавати не тільки з генератора, а й назад у нього, через аргумент next(value). Цей аргумент стає результатом того виразу yield, на якому генератор був зупинений.
function* dialog() {
const name = yield 'Як тебе звати?';
yield `Привіт, ${name}!`;
}
const it = dialog();
console.log(it.next().value); // "Як тебе звати?"
console.log(it.next('Maria').value); // "Привіт, Maria!"Перший next() лише запускає генератор до першого yield, а другий передає 'Maria' усередину як результат виразу yield.
Завершити генератор достроково можна через return:
function* example() {
yield 1;
return 'кінець';
yield 2; // не виконається
}
const it = example();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 'кінець', done: true }Асинхронні генератори і де це застосовувати
Асинхронні генератори (ES2018) дозволяють використовувати await всередині ітерованих потоків даних. Обходять їх через for await...of.
async function* fetchData() {
yield await Promise.resolve('First chunk');
yield await Promise.resolve('Second chunk');
}
for await (const part of fetchData()) {
console.log(part);
}Навіщо генератори потрібні на практиці:
| Застосування | Приклад |
|---|---|
| Створення ітераторів | Поелементна генерація даних |
| Ліниві обчислення | Не потрібно тримати весь масив у пам'яті |
| Покрокове виконання | Керування потоком виконання |
| Асинхронні пайплайни | Обробка потоків даних без завантаження пам'яті |
| Емуляція корутин | Можливість «зупинити» і «продовжити» код |
Підсумок по синтаксису і можливостях:
| Властивість | Опис |
|---|---|
| Визначення | Функція з function*, що повертає ітератор |
| Повертає | Об'єкт-генератор |
| Ключове слово | yield |
| Можна призупиняти виконання | Так |
| Можна передавати значення назад | Так |
| Асинхронна версія | async function* |
| Основне застосування | Ліниві обчислення, ітератори, асинхронні потоки |
Типові помилки
- Думати, що виклик генератора виконує його тіло.
gen()лише створює об'єкт-генератор; жоден рядок тіла не виконається, доки ви не викличетеnext(). - Плутати
yieldіreturn.returnзавершує генератор: усе, що стоїть після нього, ніколи не буде віддано, а саме значення приходить разом зdone: true. - Чекати, що перший
next(value)передасть значення всередину. Перший виклик лише доводить виконання до першогоyield, тому його аргумент ігнорується. - Обходити нескінченний генератор через
for...ofбезbreak. Цикл не завершиться ніколи, потрібен явний обмежувач. - Використовувати
for...ofзамістьfor await...ofдляasync function*. У такому разі ви отримаєте проміси, а не самі значення. - Перевикористовувати вичерпаний генератор. Після
done: trueвін не перезапускається: потрібно ще раз викликати функцію-генератор і отримати новий об'єкт.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.