← назад к разделу

Почему for..of работает с массивом, строкой, Map и Set, но не с обычным объектом? Почему spread раскладывает строку по буквам? За всем этим стоит один контракт — протокол итерации. Он маленький, и зная его, вы понимаете сразу пласт языка: от деструктуризации до генераторов.

Протокол: next() и {value, done}

Итератор — любой объект с методом next(), возвращающим { value, done }. Итерируемое — любой объект, умеющий выдать итератор через метод с ключом Symbol.iterator. Всё.

const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next: () => current <= last
        ? { value: current++, done: false }
        : { value: undefined, done: true },
    };
  },
};

for (const n of range) console.log(n); // 1 2 3
[...range];                            // [1, 2, 3]
const [first] = range;                 // 1
Math.max(...range);                    // 3

Одна реализация Symbol.iterator — и объект автоматически дружит со всем семейством потребителей: for..of, spread, деструктуризация, Array.from, new Set(...), Promise.all. Именно поэтому они одинаково работают с массивами, строками, Map, Set, NodeList — все они итерируемые. А обычный {} — нет: у него нет Symbol.iterator, для перебора объектов есть Object.keys/values/entries (которые возвращают массив — уже итерируемый).

Генераторы: итератор без бухгалтерии

Писать next() руками с ручным хранением состояния громоздко. Генератор делает то же самое синтаксисом обычной функции:

function* rangeGen(from, to) {
  for (let i = from; i <= to; i++) yield i;
}

[...rangeGen(1, 3)]; // [1, 2, 3]

Звёздочка объявляет генератор, yield «выдаёт» значение и замораживает функцию на этом месте. Каждый вызов next() размораживает её до следующего yield. Это единственная конструкция языка, где функция может остановиться посередине и продолжить позже — со всеми живыми локальными переменными.

Отсюда суперсила — ленивость. Генератор не строит всю коллекцию заранее, а выдаёт по значению по требованию:

function* ids() {
  let i = 1;
  while (true) yield i++; // бесконечный — и это нормально!
}
const gen = ids();
gen.next().value; // 1
gen.next().value; // 2 — никакого бесконечного цикла: считаем по запросу

Практичные применения: генерация идентификаторов, пагинация («выдай следующую страницу»), обход деревьев (yield внутри рекурсии через yield*), конвейеры обработки, где невыгодно материализовывать промежуточные массивы.

function* walk(node) {
  yield node;
  for (const child of node.children ?? []) yield* walk(child); // делегирование
}

yield* — «передай слово другому генератору»: рекурсивный обход дерева без ручного стека читается как обычная рекурсия.

for await..of: перебор асинхронного

Скрестим генераторы с промисами: асинхронный итератор выдаёт промисы, а for await..of их дожидается. Каноничный пример — выкачать все страницы API:

async function* fetchPages(url) {
  while (url) {
    const res = await fetch(url);
    const page = await res.json();
    yield page.items;
    url = page.next; // ссылка на следующую страницу или null
  }
}

for await (const items of fetchPages("/api/orders?page=1")) {
  render(items); // страницы обрабатываются по мере прибытия
}

Потребитель не знает ни про пагинацию, ни про количество страниц — просто перебирает. Тот же контракт «next → value/done», только асинхронный: так же устроены чтение файлов чанками в Node и потоковые ответы.

Коротко

  • Протокол итерации: итератор = next(){value, done}; итерируемое = объект с Symbol.iterator. Один контракт питает for..of, spread, деструктуризацию, Array.from.
  • Обычный объект не итерируем — перебирается через Object.keys/values/entries.
  • Генератор (function* + yield) — функция с паузами: пишется как обычный код, работает как итератор. yield* делегирует другому генератору.
  • Ленивость: значения по требованию, бесконечные последовательности, обход деревьев без материализации.
  • for await..of + async-генераторы — перебор асинхронных источников: пагинация, стримы, чанки.

Что почитать дальше

  • Промисы изнутри — асинхронная половина for await..of.
  • Замыкания — состояние генератора между вызовами живёт по тем же законам.
  • Основы JavaScript для TypeScript — мостик из цикла обратно к TypeScript-фазе программы.