Skip to main content

Генератори та yield

Генератор (function*) це функція, яка може призупиняти своє виконання, віддавати проміжні значення і потім продовжувати роботу з того самого місця. Формально function* повертає спеціальний об’єкт, ітератор, який керує виконанням тіла функції.

Теорія

TL;DR

  • function* оголошує генератор; його виклик не виконує код, а повертає об’єкт-ітератор.
  • yield призупиняє виконання, віддає значення назовні у { value, done } і чекає на наступний .next().
  • gen.next(value) повертає значення всередину: воно стає результатом того yield, на якому генератор зупинився.
  • .return(value) завершує генератор достроково, .throw(error) кидає в нього виняток, який можна впіймати через try...catch.
  • Генератор автоматично реалізує і протокол ітератора (next()), і протокол ітерованого об’єкта ([Symbol.iterator]), тому працює у for...of.
  • Генератори ідеальні для лінивих обчислень і нескінченних послідовностей: дані видаються порціями, без створення величезних масивів.

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

javascript
function* myGenerator() { yield 1; yield 2; yield 3; } const gen = myGenerator(); 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: undefined, done: true }

Як оголошується генератор і що повертає його виклик

Генератор оголошується зірочкою після ключового слова function:

javascript
function* myGenerator() { yield 1; yield 2; yield 3; }

Найважливіший момент: під час виклику myGenerator() сам код функції не виконується. Виклик повертає ітератор, який треба «прокручувати» вручну, викликаючи .next(). Кожен такий виклик доводить функцію до наступного yield і повертає об’єкт { value, done }, де value це віддане значення, а done показує, чи функція вже дійшла до кінця.

Генератором може бути і метод об’єкта чи класу, для цього зірочку ставлять перед іменем: *items() { ... }. А от стрілкова функція генератором бути не може.

Що робить yield

Оператор yield робить три речі:

  1. Призупиняє виконання функції рівно на цьому місці.
  2. Віддає значення назовні, у результат .next().
  3. Дозволяє передати значення назад усередину під час наступного виклику .next().

Приклад 1, проста послідовність:

javascript
function* counter() { yield 1; yield 2; yield 3; } const gen = counter(); 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: undefined, done: true }

Приклад 2, паузи між кроками:

javascript
function* steps() { console.log("Крок 1"); yield; console.log("Крок 2"); yield; console.log("Крок 3"); } const gen = steps(); gen.next(); // "Крок 1" gen.next(); // "Крок 2" gen.next(); // "Крок 3"

Тут yield використовується без значення, просто як точка зупинки.

Як передати значення назад у генератор

Коли ви викликаєте gen.next(value), цей value потрапляє всередину генератора саме в те місце, де виконання було призупинене оператором yield. Тобто вираз yield "..." обчислюється в те, що ви передали наступним .next().

Приклад 3, передача значення назад:

javascript
function* dialog() { const name = yield "Як тебе звати?"; const age = yield `Привіт, ${name}! Скільки тобі років?`; yield `Тобі ${age} років.`; } const gen = dialog(); console.log(gen.next()); // { value: "Як тебе звати?", done: false } console.log(gen.next("Maria")); // { value: "Привіт, Maria! Скільки тобі років?", done: false } console.log(gen.next(25)); // { value: "Тобі 25 років.", done: false } console.log(gen.next()); // { value: undefined, done: true }

Пояснення по кроках:

  • Перший next() запускає генератор і повертає значення першого yield.
  • Другий next("Maria") вставляє рядок "Maria" туди, де стояв перший yield, тому name отримує це значення.
  • Третій next(25) вставляє 25 на місце другого yield, і age дорівнює 25.

Керування генератором: next, return і throw

У ітератора генератора є три методи керування:

МетодЩо робить
.next(value)Продовжує виконання і повертає наступний { value, done }
.return(value)Примусово завершує генератор, повертає { value, done: true }
.throw(error)Кидає виняток усередину генератора, його можна впіймати через try...catch

Приклад 5, return() і throw():

javascript
function* process() { try { yield "Крок 1"; yield "Крок 2"; } catch (e) { console.log("Помилка:", e.message); } finally { console.log("Генератор завершено"); } } const gen = process(); console.log(gen.next()); // { value: "Крок 1", done: false } console.log(gen.throw(new Error("Щось пішло не так"))); // Помилка: Щось пішло не так // Генератор завершено

Блок finally спрацьовує і тоді, коли генератор завершують через .return(), тому саме туди зручно класти прибирання ресурсів.

Генератори як ітератори, ліниві та асинхронні послідовності

Генератор автоматично реалізує інтерфейс ітератора (next()) і інтерфейс ітерованого об’єкта ([Symbol.iterator]), тому його можна напряму передати у for...of, у spread або в деструктуризацію:

javascript
function* numbers() { yield 1; yield 2; yield 3; } for (const n of numbers()) { console.log(n); // 1, 2, 3 }

Приклад 4, нескінченна послідовність:

javascript
function* infinite() { let i = 0; while (true) { yield i++; } } const gen = infinite(); console.log(gen.next().value); // 0 console.log(gen.next().value); // 1 console.log(gen.next().value); // 2

Такий код не зависає, бо значення обчислюються ліниво: наступне число з’являється лише тоді, коли його попросили через .next().

Бонус, генератори і асинхронність. До появи async/await генератори використовували саме для асинхронного коду (через бібліотеки на кшталт co):

javascript
function* getData() { const user = yield fetch('/user'); const posts = yield fetch(`/posts?user=${user.id}`); return posts; }

Пізніше на цій самій ідеї і побудували async/await, по суті це «автоматизовані генератори», де рушій сам викликає .next(), коли проміс виконався.

Підсумкова таблиця:

ПоняттяОпис
function*Оголошення генератора
yieldПризупиняє виконання і віддає значення назовні
.next(value)Відновлює виконання, передаючи value всередину
.return(value)Завершує генератор
.throw(error)Кидає виняток у генератор
ГенераторОб’єкт-ітератор, виконання якого можна призупиняти і продовжувати

Типові помилки

  • Думати, що виклик myGenerator() виконує тіло функції. Він лише створює ітератор; перший рядок коду виконається тільки на першому .next().
  • Очікувати, що аргумент першого .next(value) кудись потрапить. Перший виклик лише запускає генератор, і передане значення ігнорується, бо ще немає yield, який міг би його прийняти.
  • Плутати напрямки. Назовні летить значення праворуч від yield, а всередину повертається аргумент наступного .next().
  • Прогонити нескінченний генератор через for...of без break. Цикл ніколи не завершиться, бо done не стане true.
  • Забувати, що генератор одноразовий. Після done: true або після .return() усі наступні .next() дають { value: undefined, done: true }; щоб пройти послідовність ще раз, треба створити новий генератор.
  • Намагатися оголосити генератор стрілковою функцією. function* працює лише зі звичайними функціями та методами (*method() {}).
  • Вважати генератор асинхронним сам по собі. Звичайний генератор синхронний; для асинхронних послідовностей потрібні async function* і for await...of.

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

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

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