Практическое руководство по новым методам Set в JavaScript
Новые методы Set в JavaScript: union, intersection, difference, symmetricDifference и проверки подмножества, плюс поддержка Map и браузеров.
Объект Set в JavaScript получил семь новых методов — union(), intersection(), difference(), symmetricDifference(), isSubsetOf(), isSupersetOf() и isDisjointFrom() — которые стали Baseline Newly Available начиная с 11 июня 2024 года. Эти методы приходят на смену паттернам Array.filter() + Array.includes(), которые фронтенд-разработчики годами писали вручную для сравнения коллекций, и делают это с помощью операций поиска, характерных для множеств, вместо вложенных сканирований массивов. Если вы уже знакомы с API Set из ES2015 — add, has, delete, итерация — это руководство даст вам то, что упустили анонсирующие статьи: протокол Set-подобных аргументов, позволяющий передавать Map, ловушку ссылочного равенства, которая незаметно ломает сравнение объектов, готовые к использованию фронтенд-паттерны и честную таблицу поддержки браузерами.
Ключевые выводы
- Все семь новых методов
Setимеют статус Baseline Newly Available начиная с 11 июня 2024 года: они поставляются в Chrome 122, Edge 122, Firefox 127 и Safari 17, а также в Node.js 22.0.0 — полифилл для актуальных целевых сред не требуется. - Аргумент этих методов не обязан быть экземпляром
Set; он должен предоставлять числовое свойствоsize, вызываемый методhasи вызываемый методkeys— это означает, чтоMapподходит напрямую. Setсравнивает значения по ссылке, поэтомуnew Set([{id:1}]).intersection(new Set([{id:1}]))вернёт пустое множество — пересекайте множества стабильных примитивных идентификаторов.difference()не является коммутативной операцией, аsymmetricDifference()возвращает элементы, присутствующие только в получателе, первыми — в порядке получателя.- Замена
[...a].filter(x => b.includes(x))наa.intersection(b)меняет паттерн O(n²) на массивах на вариант, масштабирующийся с размером меньшего множества при среднем сублинейном доступе кSet, как того требует спецификация.
Семь методов вкратце
Новые методы делятся на две группы: четыре возвращают новый Set, три возвращают булево значение. Это разделение — наиболее полезная ментальная модель: оно сразу даёт понять, строите ли вы коллекцию или задаёте вопрос, требующий ответа «да» или «нет».
Операции, возвращающие новый Set:
union(other)— элементы этого множества или другого (аналог SQLFULL OUTER JOIN).intersection(other)— элементы, присутствующие в обоих множествах (аналогINNER JOIN).difference(other)— элементы этого множества, которых нет в другом (аналогLEFT JOIN).symmetricDifference(other)— элементы, присутствующие в одном из множеств, но не в обоих.
Предикаты, возвращающие булево значение:
isSubsetOf(other)—true, если каждый элемент этого множества присутствует в другом.isSupersetOf(other)—true, если это множество содержит каждый элемент другого.isDisjointFrom(other)—true, если два множества не имеют общих элементов.
Предложение, добавившее эти методы, — предложение TC39 по методам Set — достигло Stage 4, в настоящее время неактивно, включено в спецификацию ECMAScript и является частью ECMAScript 2025.
Discover how at OpenReplay.com.
Использование методов, возвращающих множество, в реальном фронтенд-коде
Четыре метода, возвращающих множество, — union(), intersection(), difference() и symmetricDifference() — каждый возвращает новый Set, не изменяя ни одного из входных множеств, и каждый чётко соответствует задаче, которую фронтенд-разработчики уже решают вручную.
union(): объединение переопределений флагов функций со значениями по умолчанию
union() возвращает новое множество, содержащее каждый элемент из обоих множеств, с удалением дубликатов.
const defaults = new Set(["new-dashboard", "dark-mode"]);
const overrides = new Set(["dark-mode", "beta-search"]);
const enabled = defaults.union(overrides);
// Set(3) { "new-dashboard", "dark-mode", "beta-search" }
Классический вариант этого кода выглядел как new Set([...defaults, ...overrides]). union() выражает намерение напрямую. При объединении базового набора включённых флагов функций с пользовательскими или средовыми переопределениями union() даёт вам итоговый набор флагов за один вызов.
intersection(): поиск маршрутов, к которым пользователь действительно имеет доступ
intersection() возвращает новое множество, содержащее только элементы, присутствующие в обоих множествах.
const requiredForRoute = new Set<string>(["billing:read", "billing:write"]);
const userPermissions = new Set<string>(["billing:read", "users:read"]);
const satisfied = requiredForRoute.intersection(userPermissions);
// Set(1) { "billing:read" }
Для контроля доступа к маршрутам пересеките множество прав, требуемых маршрутом, с множеством прав пользователя. Размер результата по отношению к требуемому множеству покажет, является ли доступ частичным или полным.
difference(): отслеживание изменений выделения в компоненте множественного выбора
difference() возвращает новое множество, содержащее элементы этого множества, которых нет в другом — и эта операция не является коммутативной.
const prevSelected = new Set<string>(["a", "b", "c"]);
const nextSelected = new Set<string>(["b", "c", "d"]);
const added = nextSelected.difference(prevSelected); // Set(1) { "d" }
const removed = prevSelected.difference(nextSelected); // Set(1) { "a" }
Для компонентов множественного выбора nextSelected.difference(prevSelected) даёт только что добавленные элементы, а prevSelected.difference(nextSelected) — только что удалённые. Две операции над множествами заменяют паттерн, требовавший сортировки или вложенных циклов. a.difference(b) возвращает элементы из a, отсутствующие в b, а b.difference(a) — обратное; порядок аргументов определяет операцию.
symmetricDifference(): выделение изменившихся ключей между двумя снимками состояния
symmetricDifference() возвращает новое множество, содержащее элементы, присутствующие в одном из множеств, но не в обоих.
const before = new Set<string>(["name", "email", "phone"]);
const after = new Set<string>(["name", "email", "address"]);
const changedKeys = before.symmetricDifference(after);
// Set(2) { "phone", "address" }
Чтобы выделить ключи, которые появились или исчезли между двумя снимками объекта состояния, возьмите симметрическую разность их наборов ключей. Одна деталь, которую анонсирующие статьи обходят стороной: порядок итерации зависит от получателя. Согласно ECMA-262 2025, symmetricDifference() возвращает элементы, присутствующие только в получателе, в порядке получателя, а затем элементы, присутствующие только в другом множестве, в порядке other.keys(). Набор элементов одинаков независимо от того, с какой стороны вы вызываете метод; порядок — нет.
Использование булевых предикатов для проверок авторизации
Три метода-предиката — isSubsetOf(), isSupersetOf() и isDisjointFrom() — каждый возвращает булево значение и каждый соответствует распространённой проверке авторизации или валидации входных данных.
isSupersetOf(): проверка наличия всех необходимых областей доступа
isSupersetOf() возвращает true, если это множество содержит каждый элемент заданного множества.
const grantedScopes = new Set<string>(["read", "write", "delete"]);
const requiredScopes = new Set<string>(["read", "write"]);
const hasAllRequiredScopes = grantedScopes.isSupersetOf(requiredScopes);
// true
Чтобы проверить, покрывают ли предоставленные пользователю OAuth-области доступа все необходимые для операции области, grantedScopes.isSupersetOf(requiredScopes) возвращает true за один вызов — эквивалент [...requiredScopes].every(s => grantedScopes.has(s)), но выраженный как отношение между множествами.
isSubsetOf(): проверка полной поддержки списка тегов
isSubsetOf() возвращает true, если каждый элемент этого множества присутствует в заданном множестве.
const supportedTags = new Set<string>(["sale", "new", "featured", "clearance"]);
const requestedTags = new Set<string>(["sale", "new"]);
const allSupported = requestedTags.isSubsetOf(supportedTags);
// true
Когда вызывающая сторона передаёт список тегов или фильтров, requestedTags.isSubsetOf(supportedTags) подтверждает, что каждый из них распознан, прежде чем выполнять запрос.
isDisjointFrom(): обнаружение конфликтующих клавиш-модификаторов
isDisjointFrom() возвращает true, если два множества не имеют общих элементов.
const pressedKeys = new Set<string>(["Meta", "Shift"]);
const conflictingModifiers = new Set<string>(["Control", "Alt"]);
const noConflict = pressedKeys.isDisjointFrom(conflictingModifiers);
// true
При обработке сочетаний клавиш isDisjointFrom() позволяет убедиться, что ни один из конфликтующих клавиш-модификаторов не нажат, прежде чем выполнять действие.
Протокол Set-подобных объектов: аргумент не обязан быть Set
Аргумент любого из этих методов не обязан быть экземпляром Set — он должен быть объектом с числовым свойством size, вызываемым методом has и вызываемым методом keys. Map удовлетворяет этому требованию нативно, а значит, mySet.intersection(myMap) является валидным вызовом и выполняет проверку по ключам отображения. Каждая анонсирующая статья описывает аргумент как «другое множество», что технически неточно.
Этот протокол определён в спецификации ECMA-262 в разделе GetSetRecord, а MDN документирует требование к Set-подобным объектам напрямую.
const map = new Map([
["a", 1],
["b", 2],
]);
const set = new Set(["a", "c"]);
set.intersection(map);
// Set(1) { "a" } — проверка выполняется по ключам отображения
Подтверждено в Node v22.16.0: set.intersection(map) возвращает Set { 'a' }, поскольку "a" — единственный элемент множества, который также присутствует среди ключей отображения. Это важно для совместимости: вы можете пересечь Set с ключами Map, не создавая промежуточный new Set(map.keys()), а любая библиотека иммутабельных коллекций или пользовательский объект-индекс, предоставляющий size, has и keys, подключается к этим методам без преобразования.
Ловушка идентичности объектов
Set сравнивает значения по ссылке, а не по структуре. Документация MDN по равенству значений подтверждает это. Практическое следствие — ловушка при работе с множествами, содержащими объекты:
new Set([{ id: 1 }]).intersection(new Set([{ id: 1 }]));
// Set(0) {}
Ловушка ссылочного равенства: это возвращает пустое множество, потому что два объекта {id: 1} являются разными ссылками, даже если выглядят одинаково. Решение — пересекать множества стабильных примитивных идентификаторов, а затем восстанавливать объекты из таблицы поиска:
type User = { id: number; name: string };
const prev: User[] = [{ id: 1, name: "Ada" }, { id: 2, name: "Lin" }];
const next: User[] = [{ id: 2, name: "Lin" }, { id: 3, name: "Mo" }];
const byId = new Map(next.map((u) => [u.id, u]));
const prevIds = new Set(prev.map((u) => u.id));
const nextIds = new Set(next.map((u) => u.id));
const addedIds = nextIds.difference(prevIds); // Set(1) { 3 }
const added = [...addedIds].map((id) => byId.get(id)!);
// [{ id: 3, name: "Mo" }]
Ошибки ссылочного равенства подобного рода не оставляют стектрейса. Операция над множеством завершается без ошибки, возвращает пустое множество, и интерфейс просто не обновляется — компонент множественного выбора не реагирует на изменения, значок прав доступа не сбрасывается, представление различий не показывает изменений. Поскольку исключение не выбрасывается, ошибка не попадает в систему мониторинга. Воспроизведение сессий — это техника, которая делает подобные ошибки «тихого отказа» видимыми: вы наблюдаете, как пользователь взаимодействует с интерфейсом, операция завершается, а интерфейс никак не реагирует.
Производительность: почему это лучше filter + includes
Замена [...a].filter(x => b.includes(x)) на a.intersection(b) — это не только выигрыш в читаемости. Array.includes() имеет сложность O(n), что делает паттерн с фильтрацией O(n²) для двух коллекций из n элементов, тогда как intersection() масштабируется с размером меньшего из двух множеств, при условии среднего сублинейного доступа к Set, которого требует спецификация. Раздел спецификации ECMA-262 об объектах Set предписывает, что доступ к Set должен выполняться сублинейно в среднем — без гарантии O(1) — а справочник MDN по intersection() описывает поведение масштабирования с меньшим множеством.
| Операция | Паттерн на массивах | Сложность | Нативный метод Set | Сложность |
|---|---|---|---|---|
| Пересечение | [...a].filter(x => b.includes(x)) | O(n²) | a.intersection(b) | масштабируется с меньшим множеством, средний сублинейный доступ |
| Объединение | new Set([...a, ...b]) | O(n + m) | a.union(b) | O(n + m) |
| Разность | [...a].filter(x => !b.includes(x)) | O(n²) | a.difference(b) | масштабируется с a, средний сублинейный доступ |
Разрыв увеличивается с ростом коллекций: на небольших данных он несущественен, но паттерны на массивах деградируют квадратично, тогда как нативные методы — нет.
Рецепты миграции
Напрямую сопоставьте существующий код сравнения массивов с новыми методами. Три замены ниже покрывают большинство реальных случаев.
| Старый паттерн | Новый метод |
|---|---|
[...a].filter(x => b.includes(x)) | a.intersection(b) |
[...new Set([...a, ...b])] | a.union(b) (возвращает Set) |
[...a].filter(x => !b.includes(x)) | a.difference(b) |
Если ваши данные хранятся в массивах, оберните каждую сторону в new Set(...) один раз, выполните операцию и при необходимости разверните результат обратно в массив, если этого требует нижестоящий API:
const a = ["x", "y", "z"];
const b = ["y", "z", "w"];
const common = [...new Set(a).intersection(new Set(b))];
// ["y", "z"]
Преобразование в Set само по себе имеет сложность O(n), но такой подход позволяет избежать вложенного сканирования, присущего паттерну O(n²) с filter + includes, и лучше масштабируется по мере роста коллекций.
Поддержка браузерами и полифилы
Все семь методов Set имеют статус Baseline Newly Available начиная с 11 июня 2024 года: они поставляются в Chrome 122 (20 февраля 2024), Edge 122 (23 февраля 2024), Firefox 127 (11 июня 2024) и Safari 17 (18 сентября 2023) — полифилл для актуальных целевых браузеров не требуется. Они также доступны в Node.js 22.0.0 (24 апреля 2024) через V8 12.4.
| Среда | Версия | Дата выпуска |
|---|---|---|
| Chrome | 122 | 20 февраля 2024 |
| Edge | 122 | 23 февраля 2024 |
| Firefox | 127 | 11 июня 2024 |
| Safari | 17 | 18 сентября 2023 |
| Node.js | 22.0.0 (V8 12.4) | 24 апреля 2024 |
Для более старых целевых сред библиотека core-js и проект es-shims предоставляют соответствующие спецификации полифилы. Если вы поддерживаете только актуальные вечнозелёные браузеры и Node.js 22+, полифил можно полностью исключить.
Что делать дальше
Проверьте кодовую базу на наличие паттернов filter + includes и filter + !includes над дедуплицированными данными — это прямые кандидаты для замены на intersection() и difference(). Перед миграцией коллекций, содержащих объекты, переведите их на множества стабильных примитивных идентификаторов, чтобы избежать ловушки ссылочного равенства, и помните, что любой Map, иммутабельная коллекция или пользовательский индекс, предоставляющий size, has и keys, может быть передан в качестве аргумента напрямую. Методы имеют статус Baseline, семантика стабилизирована в спецификации, а паттерны на массивах, которые они заменяют, никогда не были такими дешёвыми, какими казались.
Часто задаваемые вопросы
Можно ли передать Map напрямую в Set.intersection() или нужно сначала преобразовать его?
Map можно передать напрямую. Новые методы Set принимают любой Set-подобный объект, определённый в спецификации ECMA-262 как объект с числовым свойством size, вызываемым методом has и вызываемым методом keys. Map удовлетворяет этому нативно, поэтому mySet.intersection(myMap) выполняет проверку по ключам отображения без создания промежуточного new Set(map.keys()). Множества из библиотек иммутабельных коллекций и пользовательские объекты-индексы, предоставляющие те же три члена, также работают без преобразования.
Почему пересечение Set возвращает пустое множество, когда оба множества содержат идентичные объекты?
Потому что Set сравнивает значения по ссылке, а не по структуре. Выражение new Set([{id:1}]).intersection(new Set([{id:1}])) возвращает пустое множество, поскольку два объекта {id:1} являются разными ссылками, даже если выглядят одинаково — что подтверждено в Node v22.16.0. Решение — создать множества стабильных примитивных идентификаторов, выполнить операцию над ними, а затем восстановить совпавшие объекты из Map, индексированного по идентификатору.
В чём разница между difference() и symmetricDifference()?
difference() — направленная и некоммутативная операция: a.difference(b) возвращает элементы из a, отсутствующие в b, а b.difference(a) — обратное. symmetricDifference() возвращает элементы, присутствующие в одном из множеств, но не в обоих, и не зависит от порядка по содержимому. Порядок итерации также различается: symmetricDifference() возвращает элементы, присутствующие только в получателе, в порядке получателя, а затем элементы, присутствующие только в другом множестве, в порядке other.keys() — таким образом, набор результатов одинаков независимо от того, с какой стороны вы вызываете метод, но порядок — нет.
Нужен ли полифил для новых методов Set в продакшене?
Для актуальных целевых браузеров — нет. Все семь методов имеют статус Baseline Newly Available начиная с 11 июня 2024 года: они поставляются в Chrome 122, Edge 122, Firefox 127, Safari 17 и Node.js 22.0.0 через V8 12.4. Если вы поддерживаете только актуальные вечнозелёные браузеры и Node.js 22 или выше, полифил можно полностью исключить. Для более старых целевых сред библиотека core-js и проект es-shims предоставляют соответствующие спецификации полифилы, которые можно подключить выборочно.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k