Kodokon kodokon.com

Итераторы, генераторы и протоколы итерации

Освой протоколы, стоящие за for...of, спред-оператором и деструктуризацией, а затем используй генераторы для ленивых последовательностей.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

Итерацией управляют два разных протокола. Объект является итерируемым, если он предоставляет метод по ключу Symbol.iterator; этот метод должен возвращать итератор, то есть объект с методом next(), возвращающим { value, done }. Всё, что потребляет последовательности - for...of, спред-оператор ..., деструктуризация, Array.from, Promise.all - проходит через эти протоколы. Поэтому ты можешь сделать совместимым любой объект.

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

console.log([...range]); // [1, 2, 3]
Итерируемый объект, написанный вручную, без классов и генераторов.

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

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

function* take(iterable, count) {
  let index = 0;
  for (const value of iterable) {
    if (index >= count) return;
    yield value;
    index += 1;
  }
}

console.log([...take(naturals(), 4)]); // [0, 1, 2, 3]
Бесконечная последовательность, потребляемая без единой блокировки.

Итераторы генераторов также предоставляют return() и throw(). Деталь спецификации, которую часто упускают из виду: когда ты выходишь из цикла for...of через break, return или исключение, движок автоматически вызывает iterator.return(). В генераторе при этом выполняются отложенные блоки finally - идеальное место, чтобы освободить ресурс (файл, соединение, блокировку).

JAVASCRIPT
function* readLines() {
  try {
    yield "line 1";
    yield "line 2";
  } finally {
    console.log("cleanup");
  }
}

for (const line of readLines()) {
  console.log(line);
  break; // triggers return(), hence the finally
}
// line 1, cleanup
break вызывает return(): блок finally выполняется.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что делает объект итерируемым в смысле протокола?
    • Наличие свойства length и числовых индексов
    • Наличие метода Symbol.iterator, который возвращает объект с next()
    • Наследование от Array.prototype
    • Наличие метода next() напрямую
  2. Что происходит, когда ты выходишь из цикла for...of через break, пока он потребляет генератор?
    • Ничего особенного, генератор остаётся приостановленным на последнем yield
    • Движок вызывает throw() у итератора
    • Движок вызывает return(), что запускает блоки finally генератора
    • Генератор начинается заново на следующей итерации
  3. Почему можно работать с бесконечным генератором, не блокируя программу?
    • Движок автоматически ограничивает число итераций
    • Генераторы выполняются в отдельном потоке
    • Вычисление ленивое: каждое значение считается только при вызове next()
    • Значения вычисляются заранее и кэшируются движком