JS генераторы

Генератор — специальная функция (function*), выполнение которой можно приостанавливать и возобновлять через ключевое слово yield, возвращающая объект-итератор.

Зачем нужно

Генераторы позволяют создавать ленивые (lazy) последовательности данных — вычислять значения по одному по мере необходимости, не загружая память целиком. Они упрощают написание кастомных итераторов: вместо ручного управления состоянием достаточно использовать yield. Генераторы лежат в основе библиотек для управления побочными эффектами (redux-saga) и удобны для описания конечных автоматов.

Где используется

  • Ленивые бесконечные последовательности: числа Фибоначчи, генераторы ID, пагинация
  • Кастомные итераторы с минимальным кодом
  • Управление асинхронным потоком (до async/await: библиотека co)
  • Конечные автоматы и протоколы обмена (двунаправленная передача через yield)
  • redux-saga: описание side-эффектов через генераторы

Основной контент

Базовый синтаксис

function* simpleGen {
  yield 1;
  yield 2;
  yield 3;
}

const gen = simpleGen;
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 }

// Генератор — итерируемый объект
for (const val of simpleGen) {
  console.log(val); // 1, 2, 3
}
console.log([...simpleGen]); // [1, 2, 3]

Бесконечная последовательность

function* fibonacci {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function take(n, iterable) {
  const result = ;
  for (const val of iterable) {
    result.push(val);
    if (result.length === n) break;
  }
  return result;
}

console.log(take(8, fibonacci)); // [0, 1, 1, 2, 3, 5, 8, 13]

Генератор уникальных ID

function* idGenerator(prefix = 'id') {
  let id = 1;
  while (true) {
    yield `${prefix}-${id++}`;
  }
}

const userIds = idGenerator('user');
console.log(userIds.next().value); // "user-1"
console.log(userIds.next().value); // "user-2"

Передача значений в генератор через next

function* calculator {
  let result = 0;
  while (true) {
    const input = yield result;
    if (input === null) break;
    result += input;
  }
  return result;
}

const calc = calculator;
calc.next();       // запускаем, первый yield, result = 0
calc.next(10);     // передаём 10, result = 10
calc.next(20);     // передаём 20, result = 30
console.log(calc.next(null)); // { value: 30, done: true }

yield* — делегирование другому генератору

function* inner {
  yield 'a';
  yield 'b';
}

function* outer {
  yield 1;
  yield* inner; // делегируем
  yield 2;
}

console.log([...outer]); // [1, 'a', 'b', 2]

return и throw

function* gen {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log('Поймали:', e.message);
    yield 'error handled';
  }
}

const g = gen;
console.log(g.next());           // { value: 1, done: false }
console.log(g.throw(new Error('Oops'))); // "Поймали: Oops", { value: 'error handled', done: false }
console.log(g.return('done'));   // { value: 'done', done: true }

Частые ошибки

  • Повторное использование генератора: после завершения (done: true) генератор нельзя «перезапустить» — нужно создать новый вызовом функции-генератора.
  • Стрелочные функции не могут быть генераторами: const gen = () => { yield 1; } — SyntaxError.
  • Забыть yield* при делегировании: yield inner вернёт объект-генератор, а не значения из него.
  • Первый next не принимает аргумент: первый вызов next(value) — значение value игнорируется, так как генератор ещё не достиг ни одного yield.

Связанные темы

Ресурсы


🎓 Источник: Генераторы и асинхронные генераторы в JavaScript

  • 📅 2019-03-05 · YouTube
  • Тезисы:
    • typeof generatorFn === 'function' — снаружи генератор не отличить от обычной функции, нужна проверка через прототип.
    • Существует скрытый класс GeneratorFunction (доступен через Object.getPrototypeOf(genFn).constructor).
    • Контекст генератора живёт как замыкание — все локальные переменные и шаги выполнения сохраняются между yield.
    • return в генераторе → { value, done: true }; yield{ value, done: false }.
    • После done: true можно повторно вызывать next — не упадёт, вернёт { value: undefined, done: true }.
    • yield* делегирует ЛЮБОМУ iterable (массив, строка, другой генератор).
    • return и throw редко применимы на практике, но полезны для отмены/инжекции ошибок в корутину.
  • Цитата:

    «Генератор — это функция, чьё выполнение можно ставить на паузу. Контекст хранится как замыкание, и при каждом next мы возвращаемся в ту же точку с теми же переменными.»


🎓 Источник: Итераторы и асинхронные итераторы в JavaScript

  • 📅 2019-03-05 · YouTube
  • Тезисы:
    • Контракт iterator = объект с next, возвращающий { value, done }.
    • Контракт iterable = объект с [Symbol.iterator], возвращающий iterator.
    • Массив, строка, Map, Set реализуют iterable — поэтому работают с for-of и spread.
    • Генератор УПРОЩАЕТ ручную реализацию iterable — императивный класс vs декларативный генератор.
    • Итератор замкнут на контекст: пока есть ссылка на iterator, его внутренние переменные не собираются GC.

🎓 Источник: Архив 2018 - Часть 8 Типизированные массивы, итераторы, генераторы

  • 📅 2020-01-06 · YouTube
  • Тезис: генераторы вокруг типизированных массивов и буферов позволяют обрабатывать большие бинарные данные ленивыми порциями без копирования.