← JavaScript/Генераторы#99 из 383← ПредыдущийСледующий →+35 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Генераторы

В базе данных 10 миллионов записей. Если загрузить их все в массив сразу — памяти не хватит. Но если отдавать по одной «по требованию» — всё работает. Это и есть идея генераторов: производить значения лениво, только когда они нужны.

Какую проблему решает

Обычная функция запускается, вычисляет всё сразу и возвращает результат. Генератор может приостанавливать выполнение на yield и возобновлять его позже. Это даёт ленивые вычисления и бесконечные последовательности без загрузки всего в память.

На основе предыдущих уроков

  • Функции — основы функций
  • Итерируемые — протокол итерации, Symbol.iterator
  • Замыкания — сохранение состояния между вызовами
  • Синтаксис и основы

    function* pipeline() {
      console.log('Шаг 1: получение данных')
      yield 'данные получены'
      console.log('Шаг 2: валидация')
      yield 'данные проверены'
      console.log('Шаг 3: сохранение')
      return 'готово'
    }
    
    const gen = pipeline()      // тело не выполнялось!
    gen.next()  // 'Шаг 1: ...' → { value: 'данные получены', done: false }
    gen.next()  // 'Шаг 2: ...' → { value: 'данные проверены', done: false }
    gen.next()  // 'Шаг 3: ...' → { value: 'готово', done: true }
    gen.next()  //               → { value: undefined, done: true }

    yield — двусторонняя связь

    В генератор можно передавать значения через next(value):

    function* accumulator() {
      let total = 0
      while (true) {
        const amount = yield total  // пауза, ждём следующее значение
        if (amount === null) return total  // сигнал завершения
        total += amount
      }
    }
    
    const acc = accumulator()
    acc.next()       // запуск: { value: 0, done: false }
    acc.next(100)    // { value: 100, done: false }
    acc.next(250)    // { value: 350, done: false }
    acc.next(null)   // { value: 350, done: true }  — завершение

    for...of с генераторами

    Генератор — итерируемый объект, работает в for...of и spread:

    function* range(from, to, step = 1) {
      for (let i = from; i <= to; i += step) yield i
    }
    
    for (const n of range(1, 5)) console.log(n)  // 1 2 3 4 5
    console.log([...range(0, 10, 2)])             // [0, 2, 4, 6, 8, 10]

    Бесконечные генераторы

    Генератор не обязан заканчиваться. Ленивая природа позволяет работать с бесконечными последовательностями:

    function* naturals(start = 1) {
      let n = start
      while (true) yield n++
    }
    
    function* fibonacci() {
      let [a, b] = [0, 1]
      while (true) {
        yield a;
        [a, b] = [b, a + b]
      }
    }
    
    // Берём только первые 5 чисел Фибоначчи — остальные не вычисляются
    const fib = fibonacci()
    for (let i = 0; i < 5; i++) console.log(fib.next().value)
    // 0, 1, 1, 2, 3

    Уникальные ID — практический пример

    function* idGenerator(prefix = '') {
      let id = 1
      while (true) yield `${prefix}${id++}`
    }
    
    const userId  = idGenerator('user-')
    const orderId = idGenerator('order-')
    
    userId.next().value   // 'user-1'
    userId.next().value   // 'user-2'
    orderId.next().value  // 'order-1'  — независимый счётчик

    return в генераторе

    function* gen() {
      yield 1
      return 'финал'  // завершает генератор, done: true
      yield 2         // никогда не выполнится
    }
    
    const g = gen()
    g.next()  // { value: 1, done: false }
    g.next()  // { value: 'финал', done: true }
    g.next()  // { value: undefined, done: true }

    Важно: for...of игнорирует значение из return — оно доступно только через next().

    Типичные ошибки

    Ошибка 1: попытка вызвать генератор как обычную функцию

    function* counter() {
      yield 1; yield 2; yield 3
    }
    
    // Неправильно — получаем генератор-объект, а не число
    const result = counter()
    console.log(result)  // Object [Generator] {} — не значения!
    
    // Правильно
    const gen = counter()
    console.log(gen.next().value)  // 1
    console.log([...counter()])    // [1, 2, 3]

    Ошибка 2: переиспользование «исчерпанного» генератора

    function* range(n) {
      for (let i = 0; i < n; i++) yield i
    }
    
    const gen = range(3)
    console.log([...gen])  // [0, 1, 2]
    console.log([...gen])  // [] — генератор уже исчерпан!
    
    // Правильно: создавай новый генератор для каждого использования
    console.log([...range(3)])  // [0, 1, 2]
    console.log([...range(3)])  // [0, 1, 2]

    Ошибка 3: бесконечный генератор в spread без ограничения

    function* inf() { let n = 0; while (true) yield n++ }
    
    // ОПАСНО — зависнет браузер/Node.js
    const all = [...inf()]  // пытается создать бесконечный массив
    
    // Правильно — бери только нужное количество
    const gen = inf()
    const first10 = Array.from({ length: 10 }, () => gen.next().value)
    // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    В реальных проектах

  • Пагинация: генератор подгружает следующую страницу по требованию
  • Уникальные ID: независимые счётчики для разных сущностей
  • Парсинг больших файлов: обработка строки за строкой без загрузки всего файла
  • Конечные автоматы: генератор как машина состояний
  • Примеры

    Генератор ID, range с шагом и ленивая пагинация данных

    // 1. Генераторы уникальных ID — независимые счётчики
    function* createIdGen(prefix = '', start = 1) {
      let id = start
      while (true) yield `${prefix}${id++}`
    }
    
    const userId  = createIdGen('user-')
    const orderId = createIdGen('order-', 1000)
    
    console.log(userId.next().value)   // 'user-1'
    console.log(userId.next().value)   // 'user-2'
    console.log(orderId.next().value)  // 'order-1000'
    console.log(orderId.next().value)  // 'order-1001'
    
    // 2. Range генератор с шагом — ленивый диапазон
    function* range(from, to, step = 1) {
      for (let i = from; i <= to; i += step) yield i
    }
    
    console.log([...range(0, 10, 2)])   // [0, 2, 4, 6, 8, 10]
    console.log([...range(1, 5)])       // [1, 2, 3, 4, 5]
    
    // 3. Ленивая пагинация — симуляция подгрузки страниц по требованию
    function* paginate(items, pageSize) {
      let page = 0
      while (page * pageSize < items.length) {
        yield items.slice(page * pageSize, (page + 1) * pageSize)
        page++
      }
    }
    
    const products = Array.from({ length: 7 }, (_, i) => ({ id: i + 1, name: `Товар ${i + 1}` }))
    const pages = paginate(products, 3)
    
    const page1 = pages.next().value
    const page2 = pages.next().value
    console.log(page1.map(p => p.name))  // ['Товар 1', 'Товар 2', 'Товар 3']
    console.log(page2.map(p => p.name))  // ['Товар 4', 'Товар 5', 'Товар 6']
    
    // 4. Бесконечный Фибоначчи — берём первые N
    function* fibonacci() {
      let [a, b] = [0, 1]
      while (true) {
        yield a;
        [a, b] = [b, a + b]
      }
    }
    
    const fib = fibonacci()
    const first8 = Array.from({ length: 8 }, () => fib.next().value)
    console.log(first8)  // [0, 1, 1, 2, 3, 5, 8, 13]

    Генераторы

    В базе данных 10 миллионов записей. Если загрузить их все в массив сразу — памяти не хватит. Но если отдавать по одной «по требованию» — всё работает. Это и есть идея генераторов: производить значения лениво, только когда они нужны.

    Какую проблему решает

    Обычная функция запускается, вычисляет всё сразу и возвращает результат. Генератор может приостанавливать выполнение на yield и возобновлять его позже. Это даёт ленивые вычисления и бесконечные последовательности без загрузки всего в память.

    На основе предыдущих уроков

  • Функции — основы функций
  • Итерируемые — протокол итерации, Symbol.iterator
  • Замыкания — сохранение состояния между вызовами
  • Синтаксис и основы

    function* pipeline() {
      console.log('Шаг 1: получение данных')
      yield 'данные получены'
      console.log('Шаг 2: валидация')
      yield 'данные проверены'
      console.log('Шаг 3: сохранение')
      return 'готово'
    }
    
    const gen = pipeline()      // тело не выполнялось!
    gen.next()  // 'Шаг 1: ...' → { value: 'данные получены', done: false }
    gen.next()  // 'Шаг 2: ...' → { value: 'данные проверены', done: false }
    gen.next()  // 'Шаг 3: ...' → { value: 'готово', done: true }
    gen.next()  //               → { value: undefined, done: true }

    yield — двусторонняя связь

    В генератор можно передавать значения через next(value):

    function* accumulator() {
      let total = 0
      while (true) {
        const amount = yield total  // пауза, ждём следующее значение
        if (amount === null) return total  // сигнал завершения
        total += amount
      }
    }
    
    const acc = accumulator()
    acc.next()       // запуск: { value: 0, done: false }
    acc.next(100)    // { value: 100, done: false }
    acc.next(250)    // { value: 350, done: false }
    acc.next(null)   // { value: 350, done: true }  — завершение

    for...of с генераторами

    Генератор — итерируемый объект, работает в for...of и spread:

    function* range(from, to, step = 1) {
      for (let i = from; i <= to; i += step) yield i
    }
    
    for (const n of range(1, 5)) console.log(n)  // 1 2 3 4 5
    console.log([...range(0, 10, 2)])             // [0, 2, 4, 6, 8, 10]

    Бесконечные генераторы

    Генератор не обязан заканчиваться. Ленивая природа позволяет работать с бесконечными последовательностями:

    function* naturals(start = 1) {
      let n = start
      while (true) yield n++
    }
    
    function* fibonacci() {
      let [a, b] = [0, 1]
      while (true) {
        yield a;
        [a, b] = [b, a + b]
      }
    }
    
    // Берём только первые 5 чисел Фибоначчи — остальные не вычисляются
    const fib = fibonacci()
    for (let i = 0; i < 5; i++) console.log(fib.next().value)
    // 0, 1, 1, 2, 3

    Уникальные ID — практический пример

    function* idGenerator(prefix = '') {
      let id = 1
      while (true) yield `${prefix}${id++}`
    }
    
    const userId  = idGenerator('user-')
    const orderId = idGenerator('order-')
    
    userId.next().value   // 'user-1'
    userId.next().value   // 'user-2'
    orderId.next().value  // 'order-1'  — независимый счётчик

    return в генераторе

    function* gen() {
      yield 1
      return 'финал'  // завершает генератор, done: true
      yield 2         // никогда не выполнится
    }
    
    const g = gen()
    g.next()  // { value: 1, done: false }
    g.next()  // { value: 'финал', done: true }
    g.next()  // { value: undefined, done: true }

    Важно: for...of игнорирует значение из return — оно доступно только через next().

    Типичные ошибки

    Ошибка 1: попытка вызвать генератор как обычную функцию

    function* counter() {
      yield 1; yield 2; yield 3
    }
    
    // Неправильно — получаем генератор-объект, а не число
    const result = counter()
    console.log(result)  // Object [Generator] {} — не значения!
    
    // Правильно
    const gen = counter()
    console.log(gen.next().value)  // 1
    console.log([...counter()])    // [1, 2, 3]

    Ошибка 2: переиспользование «исчерпанного» генератора

    function* range(n) {
      for (let i = 0; i < n; i++) yield i
    }
    
    const gen = range(3)
    console.log([...gen])  // [0, 1, 2]
    console.log([...gen])  // [] — генератор уже исчерпан!
    
    // Правильно: создавай новый генератор для каждого использования
    console.log([...range(3)])  // [0, 1, 2]
    console.log([...range(3)])  // [0, 1, 2]

    Ошибка 3: бесконечный генератор в spread без ограничения

    function* inf() { let n = 0; while (true) yield n++ }
    
    // ОПАСНО — зависнет браузер/Node.js
    const all = [...inf()]  // пытается создать бесконечный массив
    
    // Правильно — бери только нужное количество
    const gen = inf()
    const first10 = Array.from({ length: 10 }, () => gen.next().value)
    // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    В реальных проектах

  • Пагинация: генератор подгружает следующую страницу по требованию
  • Уникальные ID: независимые счётчики для разных сущностей
  • Парсинг больших файлов: обработка строки за строкой без загрузки всего файла
  • Конечные автоматы: генератор как машина состояний
  • Примеры

    Генератор ID, range с шагом и ленивая пагинация данных

    // 1. Генераторы уникальных ID — независимые счётчики
    function* createIdGen(prefix = '', start = 1) {
      let id = start
      while (true) yield `${prefix}${id++}`
    }
    
    const userId  = createIdGen('user-')
    const orderId = createIdGen('order-', 1000)
    
    console.log(userId.next().value)   // 'user-1'
    console.log(userId.next().value)   // 'user-2'
    console.log(orderId.next().value)  // 'order-1000'
    console.log(orderId.next().value)  // 'order-1001'
    
    // 2. Range генератор с шагом — ленивый диапазон
    function* range(from, to, step = 1) {
      for (let i = from; i <= to; i += step) yield i
    }
    
    console.log([...range(0, 10, 2)])   // [0, 2, 4, 6, 8, 10]
    console.log([...range(1, 5)])       // [1, 2, 3, 4, 5]
    
    // 3. Ленивая пагинация — симуляция подгрузки страниц по требованию
    function* paginate(items, pageSize) {
      let page = 0
      while (page * pageSize < items.length) {
        yield items.slice(page * pageSize, (page + 1) * pageSize)
        page++
      }
    }
    
    const products = Array.from({ length: 7 }, (_, i) => ({ id: i + 1, name: `Товар ${i + 1}` }))
    const pages = paginate(products, 3)
    
    const page1 = pages.next().value
    const page2 = pages.next().value
    console.log(page1.map(p => p.name))  // ['Товар 1', 'Товар 2', 'Товар 3']
    console.log(page2.map(p => p.name))  // ['Товар 4', 'Товар 5', 'Товар 6']
    
    // 4. Бесконечный Фибоначчи — берём первые N
    function* fibonacci() {
      let [a, b] = [0, 1]
      while (true) {
        yield a;
        [a, b] = [b, a + b]
      }
    }
    
    const fib = fibonacci()
    const first8 = Array.from({ length: 8 }, () => fib.next().value)
    console.log(first8)  // [0, 1, 1, 2, 3, 5, 8, 13]

    Задание

    Напиши генератор take(iterable, n), который принимает любой итерируемый объект (в том числе бесконечный генератор) и выдаёт только первые n элементов. Используй его для получения первых 5 чётных натуральных чисел из бесконечного генератора.

    Подсказка

    Используй return (без значения) для выхода из генератора при count >= n. Это остановит итерацию. Для чётных: take(evens(), 5). Генератор evens() уже определён в задаче.

    Загружаем среду выполнения...
    Загружаем AI-помощника...