Генераторы

Генераторы #

Генератор — особая разновидность итератора: функция function* при вызове возвращает объект-генератор с методами next, return и throw. Ключевое слово yield приостанавливает выполнение и отдаёт значение наружу. Генераторы удобны для ленивых потоков данных — постраничная загрузка товаров, обход вложенных категорий, пошаговая сборка чекаута.


Объявление и первый next() #

function* orderIdSequence(prefix) {
  let n = 1
  while (true) {
    yield `${prefix}-${String(n).padStart(4, '0')}`
    n++
  }
}

const ids = orderIdSequence('ord')
ids.next() // { value: 'ord-0001', done: false }
ids.next() // { value: 'ord-0002', done: false }

Тело генератора не выполняется при вызове orderIdSequence('ord') — только при первом next().


yield и возобновление #

Выражение после yield становится value в результате next(). Аргумент следующего next(arg) попадает в генератор как результат предыдущего yield:

function* checkoutDialog() {
  const name = yield 'Введите имя получателя'
  const phone = yield `Спасибо, ${name}. Телефон?`
  yield `Заказ оформлен для ${name}, ${phone}`
}

const dialog = checkoutDialog()
dialog.next() // { value: 'Введите имя получателя', done: false }
dialog.next('Анна') // { value: 'Спасибо, Анна. Телефон?', done: false }
dialog.next('+79991112233') // { value: 'Заказ оформлен для Анна, +79991112233', done: false }
dialog.next() // { value: undefined, done: true }

Двусторонняя передача данных через yield / next(arg) встречается реже, чем простая генерация последовательности.


return и завершение #

return внутри генератора завершает итерацию:

function* limitedOffers(offers, max) {
  for (let i = 0; i < offers.length; i++) {
    if (i >= max) return
    yield offers[i]
  }
}

[...limitedOffers(['-10%', '-15%', 'бесплатная доставка'], 2)]
// ["-10%", "-15%"]

Вызов generator.return(value) снаружи тоже завершает генератор с указанным value и done: true.


Делегирование yield* #

Передаёт итерацию другому iterable или генератору:

function* trackIds(primary, extras) {
  yield* primary
  yield* extras
}

const playlist = trackIds(
  ['t-01', 't-02'],
  ['bonus-1']
)
[...playlist] // ["t-01", "t-02", "bonus-1"]

yield* inner() эквивалентно развёртыванию всех значений inner в текущий генератор.


Генератор как iterable #

Объект-генератор уже реализует [Symbol.iterator]() — возвращает this:

function* deliveryWindows() {
  yield '10:00–14:00'
  yield '14:00–18:00'
  yield '18:00–22:00'
}

for (const slot of deliveryWindows()) {
  console.log(slot)
}

Ленивая фильтрация корзины #

function* inStockOnly(items) {
  for (const item of items) {
    if (item.stock > 0) yield item
  }
}

const catalog = [
  { sku: 'KB-01', stock: 3 },
  { sku: 'HD-07', stock: 0 },
  { sku: 'MS-03', stock: 12 },
]

for (const item of inStockOnly(catalog)) {
  console.log(item.sku)
}
// KB-01, MS-03

Элементы с нулевым остатком не обрабатываются — вычисление откладывается до запроса следующего next().


Обход дерева категорий #

function* walkCategories(node) {
  yield node.name
  for (const child of node.children ?? []) {
    yield* walkCategories(child)
  }
}

const tree = {
  name: 'Каталог',
  children: [
    { name: 'Клавиатуры', children: [] },
    { name: 'Аудио', children: [{ name: 'Наушники', children: [] }] },
  ],
}

[...walkCategories(tree)]
// ["Каталог", "Клавиатуры", "Аудио", "Наушники"]

throw в генератор #

generator.throw(err) возобновляет выполнение с выброшенной ошибкой в точке yield; если генератор её не перехватит, итерация завершится:

function* fragileStep() {
  try {
    yield 'шаг 1'
    yield 'шаг 2'
  } catch (e) {
    yield `ошибка: ${e.message}`
  }
}

const g = fragileStep()
g.next()
g.throw(new Error('сеть')) // { value: 'ошибка: сеть', done: false }

Практика: пагинация API каталога #

async function fetchPage(page) {
  // имитация ответа
  const batch = [
    { id: page * 10, title: `Товар ${page * 10}` },
    { id: page * 10 + 1, title: `Товар ${page * 10 + 1}` },
  ]
  return page < 3 ? batch : []
}

function* paginatedCatalog(fetchPage) {
  let page = 0
  while (true) {
    const batch = fetchPage(page) // синхронная заглушка; для API — async-генератор
    if (!batch.length) return
    yield* batch
    page++
  }
}

// Для реального fetch используйте async function* — см. Асинхронные итераторы

Итог #

  • function* возвращает генератор; код выполняется лениво, по вызовам next().
  • yield отдаёт значение; yield* делегирует другому iterable.
  • Генератор совместим с for...of и spread — это iterable с одноразовым проходом.
  • return и throw завершают или прерывают итерацию с контролируемым результатом.

См. также #