Skip to main content

Функції-генератори

Функція-генератор (generator function) це функція, яка може призупинити своє виконання і потім продовжити його з того самого місця. Вона повертає об'єкт-генератор і віддає значення по одному через yield, тому на ній зручно будувати ітератори, ліниві обчислення та асинхронні цикли.

Теорія

TL;DR

  • Оголошення має вигляд function* name() {}: зірочка після function робить функцію генератором.
  • Виклик не запускає тіло, а повертає об'єкт-генератор (ітератор) з методом next().
  • yield віддає значення і «заморожує» виконання; наступний next() продовжує з того самого рядка.
  • Генератор сумісний з протоколом ітератора, тому працює у for...of, спреді та деструктуризації.
  • next(value) передає значення назад усередину генератора, тож обмін двосторонній.
  • async function* разом з for await...of дають те саме для асинхронних потоків (ES2018).

Швидкий приклад

javascript
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, щоб призупиняти виконання і повертати значення по одному.

javascript
function* name(args) { // тіло функції }
  • * після function робить функцію генератором;
  • yield використовують для покрокової видачі значень;
  • під час виклику функція не виконується одразу, а повертає генератор (ітератор).

Кожен виклик next() повертає об'єкт виду { value, done }, де value це те, що віддав yield, а done показує, чи генератор уже завершився.

Генератори як ітератори

Генератор повертає об'єкт, сумісний з протоколом ітераторів. Це означає, що його можна напряму передати в for...of:

javascript
function* numbers() { yield 10; yield 20; yield 30; } for (const n of numbers()) { console.log(n); }

Вивід:

javascript
10 20 30

Так само генератор працює зі спред-оператором ([...numbers()]) і з деструктуризацією масиву.

Ліниві обчислення і нескінченні послідовності

Генератори можуть бути нескінченними: вони не створюють весь масив одразу, а видають значення «на запит». Це і є ліниві обчислення, значення генеруються в міру потреби.

javascript
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 // ...і так нескінченно

Той самий підхід зручний для скінченних послідовностей, які не варто матеріалізувати в масив:

javascript
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); }

Вивід:

javascript
1 2 3 4 5

Двосторонній обмін: next(value) і return

Дані можна передавати не тільки з генератора, а й назад у нього, через аргумент next(value). Цей аргумент стає результатом того виразу yield, на якому генератор був зупинений.

javascript
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:

javascript
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.

javascript
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 він не перезапускається: потрібно ще раз викликати функцію-генератор і отримати новий об'єкт.

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

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

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