Коллекции и итерационные протоколы
Массивы, объекты и обычный 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] = v | arr.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 |
часто size | Map |
| много вставок/удалений в hot path | Map (в 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.