Перейти к содержимому

84. Генераторы и итераторы

Генераторы — функции, которые могут приостанавливать своё выполнение и возобновлять его позже. Вместе с протоколом итерации они позволяют создавать ленивые последовательности и управлять потоком выполнения нестандартными способами.

JavaScript использует два протокола для итерации:

Итерируемый (Iterable) — объект с методом [Symbol.iterator](), который возвращает итератор.

Итератор (Iterator) — объект с методом next(), возвращающим { value, done }.

// Ручная реализация итератора
function createCounter(start, end) {
let current = start
return {
// Сам является итерируемым (возвращает себя)
[Symbol.iterator]() { return this },
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true }
}
}
}
const counter = createCounter(1, 3)
console.log(counter.next()) // { value: 1, done: false }
console.log(counter.next()) // { value: 2, done: false }
console.log(counter.next()) // { value: 3, done: false }
console.log(counter.next()) // { value: undefined, done: true }
for (const n of createCounter(1, 5)) {
console.log(n) // 1 2 3 4 5
}

Генератор — специальная функция с *. Оператор yield приостанавливает выполнение:

function* simpleGenerator() {
console.log('Шаг 1')
yield 10
console.log('Шаг 2')
yield 20
console.log('Шаг 3')
return 30
}
const gen = simpleGenerator()
console.log(gen.next()) // Шаг 1 → { value: 10, done: false }
console.log(gen.next()) // Шаг 2 → { value: 20, done: false }
console.log(gen.next()) // Шаг 3 → { value: 30, done: true }
console.log(gen.next()) // { value: undefined, done: true }

Генераторы автоматически реализуют оба протокола — их можно использовать в for...of:

function* range(from, to, step = 1) {
for (let i = from; i <= to; i += step) {
yield i
}
}
console.log([...range(1, 10, 2)]) // [1, 3, 5, 7, 9]
console.log([...range(0, 100, 25)]) // [0, 25, 50, 75, 100]

yield* передаёт управление другому итерируемому объекту:

function* numbers() { yield 1; yield 2 }
function* letters() { yield 'a'; yield 'b' }
function* combined() {
yield* numbers() // делегирует генератор numbers
yield* letters() // делегирует генератор letters
yield* [3, 4] // делегирует массив
}
console.log([...combined()]) // [1, 2, 'a', 'b', 3, 4]

gen.next(value) передаёт значение внутрь генератора — оно становится результатом yield:

function* calculator() {
let result = 0
while (true) {
const input = yield result // получаем входные данные
if (input === null) break
result += input
}
return result
}
const calc = calculator()
calc.next() // запуск генератора
calc.next(10) // передаём 10
calc.next(20) // передаём 20
calc.next(5) // передаём 5
const { value } = calc.next(null) // завершаем
console.log(value) // 35

Генераторы идеальны для бесконечных последовательностей — они ленивые и не вычисляют всё заранее:

function* fibonacci() {
let [a, b] = [0, 1]
while (true) {
yield a;
[a, b] = [b, a + b]
}
}
function take(gen, n) {
const result = []
for (const val of gen) {
result.push(val)
if (result.length >= n) break
}
return result
}
console.log(take(fibonacci(), 10))
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Бесконечный счётчик
function* naturals(start = 1) {
while (true) yield start++
}
const first5Odds = [...take(naturals(), 100)].filter(n => n % 2 !== 0).slice(0, 5)
console.log(first5Odds) // [1, 3, 5, 7, 9]
function* safeGen() {
try {
yield 1
yield 2
yield 3
} catch (err) {
console.log('Поймана ошибка:', err.message)
yield 'после ошибки'
}
}
const g = safeGen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.throw(new Error('упс!')))
// Поймана ошибка: упс!
// { value: 'после ошибки', done: false }
// gen.return() завершает генератор досрочно
const g2 = safeGen()
g2.next()
console.log(g2.return('стоп')) // { value: 'стоп', done: true }
async function* paginatedFetch(url) {
let page = 1
while (true) {
const response = await fetch(`${url}?page=${page}`)
const data = await response.json()
if (data.items.length === 0) break
yield* data.items
page++
}
}
// Использование
for await (const item of paginatedFetch('/api/users')) {
console.log(item)
// Данные загружаются по мере необходимости
}
  1. Реализуйте генератор permutations(arr) для всех перестановок массива.
  2. Создайте генератор flatten(arr) для рекурсивного развёртывания вложенных массивов.
  3. Напишите генератор zip(...iterables) — аналог Python zip().
  4. Реализуйте конечный автомат (state machine) через генератор.