Раздел 3 · JavaScript

Коллекции и итерационные протоколы

middle~60 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Коллекции и итерационные протоколы

Массивы, объекты и обычный for справляются с большинством бытовых задач: посчитать, отфильтровать, сложить в JSON и отдать клиенту. Но как только данные перестают помещаться в память, или появляется требование обходить лениво, или нужны нечисловые ключи (объект, который сам становится ключом, словарь с быстрым удалением, набор уникальных значений), массива и for уже не хватает. JavaScript отвечает на это набором коллекций (Map, Set, WeakMap, WeakSet, TypedArray) и протоколом итераторов, который связывает их между собой и с пользовательскими структурами.

Этот урок задумывался как “оглавление” коллекционного API. Что есть в стандарте, чем Map отличается от обычного объекта-словаря, когда нужен WeakRef, как работает for...of поверх итерируемого, чем generator отличается от обычной функции и зачем в 2026 году в язык въехали Iterator Helpers. Заодно посмотрим на иммутабельные методы массивов из ES2023 и на новые Object.groupBy и Map.groupBy, которые экономят целый этаж старого кода с reduce.

1. Массивы поглубже

Массив, объект с числовыми ключами и length. Из-за этого у него есть особенности.

1.1. Иммутабельные двойники методов (ES2023)

Уже упоминали в уроке 03-objects-arrays-prototypes. Ещё раз и в одной таблице:

МутируетВозвращает копию
arr.sort(cmp)arr.toSorted(cmp)
arr.reverse()arr.toReversed()
arr.splice(i, 1)arr.toSpliced(i, 1)
arr[i] = varr.with(i, v)

Используй копирующие в декларативных трансформациях. Мутирующие, в hot path с явным комментарием.

1.2. at() и отрицательные индексы

const xs = [10, 20, 30];
xs.at(-1);       // 30, последний
xs.at(-2);       // 20
xs[xs.length - 1]; // 30, старая запись

at, идиоматичная замена xs[xs.length - 1]. Поддерживается в строках, TypedArrays и массивах.

1.3. flat и flatMap

[1, [2, [3, [4]]]].flat();         // [1, 2, [3, [4]]] (depth: 1)
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]

['a b', 'c d'].flatMap((s) => s.split(' ')); // ['a', 'b', 'c', 'd']

flatMap, эквивалент map(...).flat(1), но в один проход и с понятной семантикой “функция возвращает массив, склеиваем”.

1.4. Создание массивов

new Array(3);              // [ <3 empty items> ]  sparse, плохо
new Array(3).fill(0);      // [0, 0, 0]
Array.from({ length: 3 }); // [undefined, undefined, undefined]
Array.from({ length: 3 }, (_, i) => i); // [0, 1, 2]
Array.of(3);               // [3]   (vs new Array(3), пустой массив длины 3)

Array.from(iterable, mapper), удобный способ построить массив из итератора, NodeList, Set, Map, генератора. Шортерт [...iter] тоже работает, но без mapper.

2. Map и Set

Object, удобный для строковых ключей. Когда ключи нестрингового типа, нужен порядок вставки или нужна O(1) size, бери Map.

2.1. Map

const m = new Map();
m.set('a', 1);
m.set({ id: 2 }, 'object as key');
m.set(NaN, 'NaN as key');         // да, можно
m.size;                            // 3
m.get('a');                        // 1
m.has('a');                        // true
m.delete('a');                     // true
m.clear();

Что Map даёт сверх обычного объекта:

  • ключи любого типа (строки, числа, объекты, Symbol, NaN);
  • порядок итерации = порядок вставки;
  • m.size за O(1) (у объекта надо считать Object.keys(o).length);
  • никаких прототипных коллизий (o['constructor'] сюрпризов).

Итерация:

for (const [k, v] of m) console.log(k, v);     // [Map Iterator]
m.forEach((v, k) => console.log(k, v));
m.keys();                                       // iterator ключей
m.values();                                     // iterator значений
m.entries();                                    // iterator пар (то же, что for...of)

Конвертации:

const m = new Map([['a', 1], ['b', 2]]);
const obj = Object.fromEntries(m);             // { a: 1, b: 2 }
const m2 = new Map(Object.entries({ a: 1 }));  // обратно

Когда брать Map vs Object:

СлучайБери
ключи строки, фиксированный наборобъект
ключи произвольные (объекты, Symbol, числа)Map
важен порядокMap
часто sizeMap
много вставок/удалений в hot pathMap (в V8 быстрее)
JSON-сериализация из коробкиобъект

2.2. Set

Дедуплицированная коллекция:

const s = new Set([1, 2, 2, 3]);
s.size;                  // 3
s.has(2);                // true
s.add(4);
s.delete(1);
[...s];                  // [2, 3, 4]

Equality семантика, как у === (с правкой на NaN: Set считает все NaN равными). Объекты сравниваются по ссылке:

const s = new Set();
s.add({ id: 1 });
s.add({ id: 1 });
s.size;       // 2, разные ссылки

Если нужна структурная дедупликация, делай через ключ:

const dedupBy = (xs, key) => {
  const seen = new Set();
  return xs.filter((x) => {
    const k = key(x);
    if (seen.has(k)) return false;
    seen.add(k);
    return true;
  });
};

ES2025 принёс операции над множествами:

const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

a.union(b);              // Set {1, 2, 3, 4}
a.intersection(b);       // Set {2, 3}
a.difference(b);         // Set {1}
a.symmetricDifference(b);// Set {1, 4}
a.isSubsetOf(b);         // false
a.isSupersetOf(b);       // false
a.isDisjointFrom(b);     // false

В Node 22+ доступны нативно, в Node 20 за флагом.

2.3. WeakMap и WeakSet

WeakMap и WeakSet хранят ссылку на объект слабо: GC может удалить объект, даже если он есть в WeakMap-ключах.

const cache = new WeakMap();

function describe(obj) {
  if (cache.has(obj)) return cache.get(obj);
  const desc = `${obj.kind}: ${obj.name}`;
  cache.set(obj, desc);
  return desc;
}

// когда obj выйдет из других scope, WeakMap его освободит

Применения:

  • кэш, привязанный к объекту (не утекает после исчезновения объекта);
  • private state для инстанса в legacy без #;
  • side-channel метаданные (например, “был ли объект уже валидирован”).

Ограничения:

  • ключи только объекты (не строки, не числа);
  • нет size, нет итерации, нет forEach (потому что GC может удалить ключ в любой момент, и это нарушит “стабильность” обхода).

WeakSet, та же логика, но без значений: членство в множестве, которое не препятствует GC.

3. Iterable / Iterator protocol

Iterable, любой объект, реализующий метод [Symbol.iterator](). Этот метод возвращает iterator, объект с методом next(), возвращающим { value, done }.

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

for (const x of myRange) console.log(x); // 1, 2, 3, 4, 5
[...myRange];                             // [1, 2, 3, 4, 5]

Любой for...of, любой [...spread], любая деструктуризация const [a, b] = ... под капотом дёргает Symbol.iterator. Поэтому любой ваш custom-объект, реализующий протокол, прозрачно работает с языковыми конструкциями.

Все встроенные коллекции уже iterable: Array, Map, Set, String, TypedArray, arguments, NodeList. Поэтому одинаково работают:

[...'hi'];           // ['h', 'i']
[...new Set([1, 2])]; // [1, 2]

4. Generator

Generator, синтаксический сахар над iterator. Объявляется через function*, внутри yield приостанавливает выполнение и возвращает значение.

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

[...range(1, 5)];     // [1, 2, 3, 4, 5]

const g = range(1, 3);
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }

Генератор сам реализует Symbol.iterator, его можно класть в for...of напрямую. И он ленив: следующее значение вычисляется только при запросе.

yield* делегирует другому iterable:

function* concat(...iters) {
  for (const it of iters) yield* it;
}

[...concat([1, 2], new Set([3, 4]))]; // [1, 2, 3, 4]

Применения:

  • бесконечные последовательности (function* naturals() { let i = 1; while (true) yield i++; });
  • ленивая обработка больших файлов (for (const line of readLines(file)));
  • декомпозиция логики, в Effect-TS на этом построены Effect.gen-блоки (см. 02-cs/08-fp-effect-systems);
  • coroutine-стиль, временное приостановление с двусторонним обменом (g.next(value) пробрасывает value обратно как результат yield).

Протокол и генератор вместе дают ленивую нарезку на пачки: коротко, но с двумя ловушками (повторный обход и момент проверки аргумента). Напиши её в редакторе ниже, тесты гоняются прямо в браузере.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.