Iterator Pattern (GoF) — Итератор

Унифицированный интерфейс обхода коллекции или потока. В JS встроено через Symbol.iterator, Symbol.asyncIterator, for...of, for await...of.

Проблема

Разные коллекции (массив, set, дерево, стрим, БД-курсор) имеют разную структуру. Хочется писать код, который обходит любую из них одинаково: for (const x of collection).

Плюс: коллекция может не помещаться в память (большой файл, поток событий) — нужен ленивый обход.

Где используется

  • for...of, spread [...iter], деструктуризация
  • Ленивые последовательности и бесконечные потоки данных
  • Кастомные коллекции (связные списки, деревья, графы)
  • Генераторы как удобный способ создания итераторов
  • Асинхронная итерация через for await...of

Решение

  • Объект реализует Symbol.iterator — возвращает объект с методом next.
  • next возвращает { value, done }.
  • Для async-источников — Symbol.asyncIterator + next возвращает Promise.

Реализации

Базовый протокол итератора

const arrayIter = [1, 2, 3][Symbol.iterator];
arrayIter.next(); // { value: 1, done: false }
arrayIter.next(); // { value: 2, done: false }
arrayIter.next(); // { value: 3, done: false }
arrayIter.next(); // { value: undefined, done: true }

Кастомный итератор (вручную)

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

  [Symbol.iterator] {
    let current = this.start();
    const end = this.end();
    return {
      next {
        return current <= end
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
}

const range = new Range(1, 5);
for (const n of range) console.log(n); // 1..5
console.log([...range]); // [1, 2, 3, 4, 5]

Через генератор (проще)

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

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

// Бесконечный поток
function* naturals {
  let n = 1;
  while (true) yield n++;
}
const first10 = Array.from({ length: 10 }, (_, i) => i + 1);

Связный список

class LinkedList {
  constructor { this.head = null; }
  push(value) { this.head = { value, next: this.head }; }

  [Symbol.iterator] {
    let current = this.head;
    return {
      next {
        if (current) {
          const value = current.value;
          current = current.next();
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

Async iterator (потоки)

async function* readLines(stream) {
  let buffer = '';
  for await (const chunk of stream) {
    buffer += chunk;
    const lines = buffer.split('\n');
    buffer = lines.pop();
    for (const line of lines) yield line;
  }
  if (buffer) yield buffer;
}

for await (const line of readLines(fs.createReadStream('file.txt'))) {
  console.log(line);
}

Где используется в JS-экосистеме

  • Array/Set/Map/String — все имеют Symbol.iterator
  • fs.createReadStreamfor await обходит чанки
  • AsyncIterator для пагинированных API: for await (const item of paginate)
  • Generatorsfunction* создаёт итератор автоматически
  • DOM NodeList — итерируемая, но не массив

Подводные камни

  • Iterator одноразовый — пройти второй раз нельзя, нужно создать новый.
  • Spread оператор ([...iter]) исчерпывает итератор.
  • Не все iterable — Array-like (NodeList — iterable, но без .map).
  • for await в for...of для sync-итераторов работает, но не наоборот.
  • Бесконечный итератор без ограничения[...infiniteGen] зависнет.
  • Путаница итератора и итерируемого — итерируемый объект имеет [Symbol.iterator], итератор имеет next. Они могут совпадать (генератор), но не всегда.

Главные тезисы автора

  • «Унифицированный интерфейс для обхода коллекции или потока».
  • «Если коллекция в память не помещается — итератор позволяет обходить по частям».
  • Стримы реализуют интерфейс итератораfor await ходит по readable streams.
  • Стримы наследуют от EventEmitter — итератор + Observer в одном.
  • Примеры применения: видео, события игры, чанки большого файла.

🎓 Источники

См. также