JavaScript · Синтаксис · Продвинутый уровень

Итераторы и генераторы

10 задач

Создавайте собственные итерируемые объекты через `Symbol.iterator`, пишите функции-генераторы с `function*` и `yield`, используйте `yield*` для делегирования.

Протокол итератора, ленивые вычисления и функции-генераторы

#
**Зачем существуют итераторы** Массивы нетерпеливы — они хранят все значения в памяти сразу. Итераторы ленивы — они производят одно значение за раз, по требованию. Это важно для больших наборов данных, бесконечных последовательностей или дорогих вычислений где нужна лишь часть значений: ```js // Массив: все 1 000 000 чисел сразу в памяти const arr = Array.from({ length: 1_000_000 }, (_, i) => i); // Генератор: одно число на вызов .next(), почти не использует память function* range(n) { for (let i = 0; i < n; i++) yield i; } for (const x of range(1_000_000)) { /* ... */ } ``` **Протокол итератора** Объект является *iterable* если имеет метод `[Symbol.iterator]()` возвращающий *iterator*. Итератор — объект с методом `next()` возвращающим `{ value, done }`: ```js const counter = { [Symbol.iterator]() { let n = 1; return { next() { return n <= 3 ? { value: n++, done: false } : { value: undefined, done: true }; } }; } }; for (const x of counter) console.log(x); // 1 2 3 console.log([...counter]); // [1, 2, 3] ``` Встроенные iterables: `Array`, `String`, `Map`, `Set`, `arguments`, `NodeList`. **Функции-генераторы — более простой способ** Писать итераторы вручную повторяемо. Функции-генераторы (`function*`) позволяют записать ту же логику с гораздо меньшим кодом. Каждый `yield` приостанавливает выполнение и отправляет значение наружу: ```js function* counter(start, end) { for (let i = start; i <= end; i++) { yield i; // пауза здесь, отправить i наружу } } for (const n of counter(1, 3)) console.log(n); // 1 2 3 console.log([...counter(1, 5)]); // [1, 2, 3, 4, 5] const g = counter(1, 2); g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: undefined, done: true } ```

yield*, передача значений через .next(), async генераторы

#
**`yield*` — делегирование другому iterable** ```js function* abc() { yield 'a'; yield 'b'; yield 'c'; } function* nums() { yield 1; yield 2; } function* combined() { yield* abc(); // делегирует: производит 'a', 'b', 'c' yield* nums(); // затем 1, 2 yield* [3, 4]; // работает с любым iterable } console.log([...combined()]); // ['a','b','c',1,2,3,4] ``` **Метод-генератор класса — делаем объекты iterable** ```js class Tree { constructor(value, children = []) { this.value = value; this.children = children; } *[Symbol.iterator]() { yield this.value; for (const child of this.children) { yield* child; // рекурсивное делегирование } } } const tree = new Tree(1, [new Tree(2, [new Tree(4)]), new Tree(3)]); console.log([...tree]); // [1, 2, 4, 3] ``` **Передача значений в генератор через `.next(value)`** Значение переданное в `.next(value)` становится *результатом* выражения `yield` внутри генератора — двусторонняя коммуникация: ```js function* accumulator() { let total = 0; while (true) { const n = yield total; // отправляет total; получает следующий .next(n) if (n === null) return total; total += n; } } const acc = accumulator(); acc.next(); // { value: 0, done: false } acc.next(10); // { value: 10, done: false } acc.next(20); // { value: 30, done: false } acc.next(null);// { value: 30, done: true } ``` **Async генераторы — пагинация, потоки** ```js async function* paginate(url) { let page = 1; while (true) { const res = await fetch(`${url}?page=${page++}`); const json = await res.json(); if (!json.items.length) break; yield* json.items; } } for await (const user of paginate('/api/users')) { console.log(user.name); } ```

Таблица Iterator/Iterable/Generator, потребители, бесконечные последовательности

#
**Iterator vs Iterable vs Generator** | Концепция | Имеет | Используется с | |---|---|---| | Iterable | `[Symbol.iterator]()` | `for...of`, spread, деструктуризация | | Iterator | `.next()` → `{value, done}` | Прямые вызовы `.next()` | | Generator | оба (является Iterator + Iterable) | оба | **Места где iterables потребляются** ```js for (const x of iter) {} // for...of const arr = [...iter]; // spread const [a, b, ...rest] = iter; // деструктуризация Array.from(iter) // Array.from new Set(iter) / new Map(iter) // конструкторы Set/Map Promise.all(iter) // Promise-комбинаторы ``` **Бесконечные генераторы** ```js function* naturals(start = 1) { while (true) yield start++; } function* take(n, iterable) { let count = 0; for (const x of iterable) { if (count++ >= n) return; yield x; } } console.log([...take(5, naturals())]); // [1, 2, 3, 4, 5] ``` **Раннее завершение: `return()` и `finally`** ```js function* withCleanup() { try { yield 1; yield 2; yield 3; } finally { console.log('cleanup!'); } } const g = withCleanup(); g.next(); // { value: 1, done: false } g.return('end'); // 'cleanup!' -> { value: 'end', done: true } for (const x of withCleanup()) { if (x === 1) break; // 'cleanup!' тоже печатается } ``` **Генератор vs Массив** | Ситуация | Использовать | |---|---| | Все значения нужны сразу | Массив | | Большие/бесконечные данные | Генератор | | Async источник (API, потоки) | Async генератор | | Нужны `.map`/`.filter`/`.reduce` | Массив |
01

Реализовать протокол итератора

#

Создайте объект `Counter`, реализующий протокол итератора вручную. Он должен считать от `start` до `end` (включительно). Добавьте метод `[Symbol.iterator]()`, возвращающий объект с методом `next()`. Используйте в цикле `for...of`.

function makeCounter(start, end) {
  return {
    [Symbol.iterator]() {
      // верните объект итератора с next()
    }
  };
}

for (const n of makeCounter(1, 5)) {
  console.log(n);  // 1 2 3 4 5
}
Решение
function makeCounter(start, end) {
  return {
    [Symbol.iterator]() {
      let current = start;
      return {
        next() {
          if (current <= end) {
            return { value: current++, done: false };
          }
          return { value: undefined, done: true };
        }
      };
    }
  };
}

for (const n of makeCounter(1, 5)) {
  console.log(n);  // 1 2 3 4 5
}
02

Функция-генератор с function*

#

Напишите функцию-генератор `range(start, end, step = 1)`, выдающую числа от `start` до `end` (не включая), с шагом `step`. Используйте для вывода чисел 0, 2, 4, 6, 8.

function* range(start, end, step = 1) {
  // выдавайте числа здесь
}

for (const n of range(0, 10, 2)) {
  console.log(n);  // 0 2 4 6 8
}
Решение
function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

for (const n of range(0, 10, 2)) {
  console.log(n);  // 0 2 4 6 8
}
03

Бесконечный генератор: Фибоначчи

#

Напишите бесконечный генератор `fibonacci()`, выдающий последовательность Фибоначчи бесконечно (0, 1, 1, 2, 3, 5, 8, ...). Используйте его для вывода первых 8 чисел Фибоначчи через `.next()` или выхода из цикла `for...of` со счётчиком.

function* fibonacci() {
  // бесконечная последовательность
}

const gen = fibonacci();
for (let i = 0; i < 8; i++) {
  console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
Решение
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const gen = fibonacci();
for (let i = 0; i < 8; i++) {
  console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
04

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

#

Напишите два генератора: `odds(n)` выдаёт нечётные числа 1, 3, 5, ..., до n; `evens(n)` выдаёт чётные числа 2, 4, 6, ..., до n. Напишите третий генератор `oddsAndEvens(n)`, использующий `yield*` сначала для делегирования `odds(n)`, затем `evens(n)`.

function* odds(n) {
  for (let i = 1; i <= n; i += 2) yield i;
}

function* evens(n) {
  for (let i = 2; i <= n; i += 2) yield i;
}

function* oddsAndEvens(n) {
  // используйте yield* здесь
}

console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
Решение
function* odds(n) {
  for (let i = 1; i <= n; i += 2) yield i;
}

function* evens(n) {
  for (let i = 2; i <= n; i += 2) yield i;
}

function* oddsAndEvens(n) {
  yield* odds(n);
  yield* evens(n);
}

console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
05

Сделать класс итерируемым

#

Создайте класс `NumberRange` с `constructor(start, end)`. Добавьте `[Symbol.iterator]()` как метод-генератор, чтобы экземпляры работали с `for...of`, spread и деструктуризацией.

class NumberRange {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    // выдавайте значения здесь
  }
}

const r = new NumberRange(1, 5);
console.log([...r]);        // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b);          // 1 2
for (const n of r) console.log(n);
Решение
class NumberRange {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const r = new NumberRange(1, 5);
console.log([...r]);        // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b);          // 1 2
for (const n of r) console.log(n);
06

Конвейер генераторов

#

Постройте конвейер из трёх генераторов: `naturals()` — бесконечный: 1, 2, 3, ...; `take(gen, n)` — первые n значений из генератора; `squares(gen)` — отображает каждое значение в его квадрат. Выведите первые 5 квадратов через `take(squares(naturals()), 5)`.

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(gen, n) {
  // выдайте первые n значений из gen
}

function* squares(gen) {
  // выдайте x*x для каждого x из gen
}

console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
Решение
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(gen, n) {
  let count = 0;
  for (const val of gen) {
    if (count++ >= n) break;
    yield val;
  }
}

function* squares(gen) {
  for (const x of gen) yield x * x;
}

console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
07

Возвращаемое значение генератора

#

Напишите генератор `countdown(n)`, выдающий n, n-1, ..., 1, а затем возвращающий `'Done!'`. Покажите, что значение return появляется в последнем вызове `.next()` как `{ value: 'Done!', done: true }`, но НЕ выдаётся `for...of`.

function* countdown(n) {
  while (n > 0) yield n--;
  return 'Done!';
}

const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'Done!', done: true }

for (const n of countdown(3)) {
  process.stdout.write(n + ' ');
}
Решение
function* countdown(n) {
  while (n > 0) yield n--;
  return 'Done!';
}

const gen = countdown(3);
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next()); // { value: 'Done!', done: true }

for (const n of countdown(3)) {
  process.stdout.write(n + ' ');
}
08

Spread и деструктуризация с пользовательскими итерируемыми

#

Создайте класс `LinkedList`, где каждый узел имеет `value` и `next`. Добавьте `[Symbol.iterator]()`, чтобы список можно было развернуть в массив и деструктурировать. Постройте список 1 → 2 → 3 и убедитесь, что `[...list]` даёт `[1, 2, 3]`.

class LinkedList {
  constructor() { this.head = null; }

  push(value) {
    this.head = { value, next: this.head };
    return this;
  }

  *[Symbol.iterator]() {
    // обход от head
  }
}

const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]);         // [1, 2, 3]
const [first, second] = list;
console.log(first, second);     // 1 2
Решение
class LinkedList {
  constructor() { this.head = null; }

  push(value) {
    this.head = { value, next: this.head };
    return this;
  }

  *[Symbol.iterator]() {
    let node = this.head;
    while (node) {
      yield node.value;
      node = node.next;
    }
  }
}

const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]);         // [1, 2, 3]
const [first, second] = list;
console.log(first, second);     // 1 2
09

Асинхронный генератор с for await...of

#

Напишите асинхронный генератор `asyncRange(start, end, delay)`, выдающий числа от `start` до `end`, ожидая `delay` мс между каждым. Потребляйте его через `for await...of` внутри асинхронной функции `main()`.

async function* asyncRange(start, end, delay) {
  for (let i = start; i <= end; i++) {
    await new Promise(r => setTimeout(r, delay));
    yield i;
  }
}

async function main() {
  // используйте for await...of здесь
}

main();
Решение
async function* asyncRange(start, end, delay) {
  for (let i = start; i <= end; i++) {
    await new Promise(r => setTimeout(r, delay));
    yield i;
  }
}

async function main() {
  for await (const n of asyncRange(1, 4, 50)) {
    console.log(n);  // 1 2 3 4
  }
}

main();
10

Добавить Symbol.iterator к обычному объекту

#

Имея `const deck = { suits: ['♠','♥','♦','♣'], values: ['A','2','3'] }`, добавьте метод-генератор `[Symbol.iterator]`, выдающий все комбинации карт (`'A♠'`, `'A♥'`, ..., `'3♣'`). Проверьте через `[...deck]`.

const deck = {
  suits: ['♠', '♥', '♦', '♣'],
  values: ['A', '2', '3'],
  *[Symbol.iterator]() {
    // выдайте все комбинации значений и мастей
  }
};

console.log([...deck].length);   // 12
console.log([...deck][0]);        // 'A♠'
console.log([...deck][11]);       // '3♣'
Решение
const deck = {
  suits: ['♠', '♥', '♦', '♣'],
  values: ['A', '2', '3'],
  *[Symbol.iterator]() {
    for (const value of this.values) {
      for (const suit of this.suits) {
        yield `${value}${suit}`;
      }
    }
  }
};

console.log([...deck].length);   // 12
console.log([...deck][0]);        // 'A♠'
console.log([...deck][11]);       // '3♣'