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

Протокол итерации — соглашение JavaScript, по которому объект считается итерируемым (iterable), если у него есть метод Symbol.iterator, возвращающий итератор с методом next, который возвращает { value, done }.

Зачем нужно

Протокол итерации лежит в основе for...of, spread-оператора, деструктуризации, Array.from, Promise.all и других конструкций ES6+. Понимание протокола позволяет создавать собственные итерируемые структуры данных и работать с генераторами.

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

  • for...of работает с любым итерируемым объектом
  • Spread [...iterable] и деструктуризация const [a, b] = iterable
  • Кастомные коллекции: диапазоны чисел, деревья, графы
  • Генераторы реализуют оба протокола автоматически

Протоколы

Iterable protocol — объект с методом [Symbol.iterator], возвращающим итератор.

Iterator protocol — объект с методом next, возвращающим { value: any, done: boolean }.

// Встроенные итерируемые: Array, String, Map, Set, NodeList
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator];

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Создание кастомного итератора

Диапазон чисел (Range)

function range(start, end, step = 1) {
  return {
    [Symbol.iterator] {
      let current = start;
      return {
        next {
          if (current <= end) {
            const value = current;
            current += step;
            return { value, done: false };
          }
          return { value: undefined, done: true };
        }
      };
    }
  };
}

// Теперь range итерируемый:
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]
const [first, second] = range(10, 20, 5); // 10, 15

Итерируемый класс

class LinkedList {
  #head = null;

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

  [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 };
      }
    };
  }
}

const list = new LinkedList();
list.append(3).append(2).append(1);

for (const val of list) {
  console.log(val); // 1, 2, 3
}

console.log([...list]); // [1, 2, 3]

Генератор как итератор

// Генераторы реализуют оба протокола автоматически
function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

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

Частые ошибки

  • Итератор ≠ итерируемый — итератор имеет next, итерируемый — Symbol.iterator; генераторы совмещают оба протокола, обычные итераторы — нет.
  • Однократное использование итератора — итератор отслеживает состояние; повторно перебрать его нельзя, нужно создать новый через [Symbol.iterator].
  • Бесконечный итератор без ограничения — итератор без done: true бесконечен; [...infiniteRange] вызовет зависание.

Связанные темы

Ресурсы


🎓 Источник: Итерирование, циклы и итераторы в JavaScript

  • 📅 2018-11-05 · YouTube
  • Тезисы: iterator pattern из GoF реализован в JS как protocol. for...of, spread, destructuring, Array.from — все они используют Symbol.iterator. Один объект может быть iterator И iterable (return this в Symbol.iterator)

🎓 Источник: Итераторы и асинхронные итераторы в JavaScript

  • 📅 2019-03-05 · YouTube
  • Тезисы: Symbol.asyncIterator возвращает Promise<{value, done}>. Работает с for await...of. Используется в Node.js Readable streams и fetch body