Когда нужно обработать теги поста и оставить только уникальные значения, половина разработчиков тянется к привычным методам массивов, а потом удивляется, почему код работает медленнее, чем хотелось бы. На самом деле задача решается в одну строку, если знать нужный инструмент.
В этой статье разберёмся, почему Set - это не просто красивое решение, а настоящий выигрыш по производительности. Посмотрим, как он работает под капотом, какие ошибки подстерегают новичков, и когда Array всё же может быть полезнее.
Чем Set отличается от Array
Массив - это индексированная коллекция. Значения упорядочиваются по позиции: нулевой элемент, первый, второй. Каждый раз, когда нужно проверить, есть ли уже такой элемент, приходится либо использовать indexOf(), либо перебирать весь массив циклом. Для массива с тысячей тегов это быстро становится боль.
Set - это совсем другое животное. Это коллекция ключей, где каждое значение хранится только один раз. Когда вы пытаетесь добавить дубликат, Set просто его проигнорирует. Нет никаких проверок перед вставкой - это встроено в саму структуру данных.
- Индексированность: Array упорядочивает по индексам (0, 1, 2…), Set упорядочивает по ключам
- Уникальность: Array хранит всё, Set автоматически отбрасывает дубликаты
- Производительность проверки: Array требует линейного поиска, Set проверяет за O(1)
- Порядок элементов: Оба сохраняют порядок вставки, но только Array позволяет быстро обратиться по индексу
Как Set обрабатывает примитивы и объекты
Вот тут начинается самое интересное. Set работает с примитивами (строки, числа, boolean) по значению, а с объектами по ссылке.
Если у вас массив тегов вроде ['react', 'vue', 'react', 'angular'] - Set справится за миллисекунду. Преобразовываем в Set и обратно в массив:
const tags = ['react', 'vue', 'react', 'angular'];
const uniqueTags = [...new Set(tags)];
// Результат: ['react', 'vue', 'angular']
Но если вы решили хранить теги как объекты {name: 'react'}, вот тут Set ловит косяк. Даже если два объекта выглядят идентично, это разные ссылки в памяти:
const tagObjects = [{id: 1}, {id: 1}, {id: 1}];
const uniqueTagObjects = new Set(tagObjects);
console.log(uniqueTagObjects.size); // 3, не 1!
Почему так? Потому что Set сравнивает объекты по ссылке, а не по содержимому. Каждый {id: 1} - это разные адреса в памяти. Если вам действительно нужно дедублицировать объекты, придётся писать свою логику или использовать Map с кастомным ключом.
- Примитивы: Сравниваются по значению - два одинаковых числа считаются дубликатом
- Объекты: Сравниваются по ссылке - два объекта с одинаковыми свойствами это разные элементы
- Смешанные типы:
1 (число) и '1' (строка) - разные значения, Set оба сохранит
- Null и undefined: Сохраняются по одному экземпляру
Производительность: цифры говорят сами за себя
Если вы слышали, что Set быстрее - это не маркетинг, это факт. Тесты на реальных объёмах данных показывают: Set быстрее Array с filter в десятки раз, а reduce в сотню раз.
Когда вы вызываете arr.indexOf() или arr.includes() для проверки наличия элемента, браузер проходит весь массив с начала. Это O(n) операция. Повторяем проверку для каждого элемента - получаем O(n²). Set использует хеш-таблицу внутри и проверяет за O(1). Для массива из тысячи элементов разница измеряется в порядках величины.
Сравните эти подходы:
| Подход |
Код |
Скорость |
| Set |
[...new Set(arr)] |
Базовая |
| Filter + indexOf |
arr.filter((v, i) => arr.indexOf(v) === i) |
~10x медленнее |
| Reduce |
arr.reduce((acc, v) => acc.includes(v) ? acc : [...acc, v], []) |
~100x медленнее |
| Цикл с проверкой |
for + arr.includes() |
~20x медленнее |
// Измеряем самый быстрый способ
const testArray = Array.from({length: 10000}, (_, i) => i % 100);
console.time('Set approach');
const result1 = [...new Set(testArray)];
console.timeEnd('Set approach'); // ~1ms
console.time('Filter approach');
const result2 = testArray.filter((v, i) => testArray.indexOf(v) === i);
console.timeEnd('Filter approach'); // ~50ms
Методы Set для работы с тегами
Kогда вы знаете, какие методы есть, работать с Set становится проще. Вот что вам нужно для обработки тегов:
set.add(value) - добавить тег в множество, дубликаты игнорируются автоматически
set.has(value) - проверить, есть ли тег уже в множестве (быстро, за O(1))
set.delete(value) - удалить конкретный тег
set.clear() - очистить всё множество
set.size - узнать количество уникальных тегов
Практический пример - обработка тегов поста:
const postTags = new Set();
// Добавляем теги пользователя
postTags.add('javascript');
postTags.add('frontend');
postTags.add('javascript'); // Игнорируется
// Проверяем наличие
if (postTags.has('javascript')) {
console.log('Тег уже есть');
}
// Итерируем в порядке добавления
postTags.forEach(tag => console.log(tag));
// javascript, frontend
// Удаляем ненужное
postTags.delete('frontend');
console.log(postTags.size); // 1
Итерация всегда происходит в порядке вставки элементов. Это важно - если вы хотите сохранить порядок тегов, как их вводил пользователь, Set не подведёт.
Когда Array ещё может быть полезен
Не преувеличивайте, Set - не серебряная пуля. Есть сценарии, где Array всё ещё нужен.
Если вам нужно частно обращаться по индексу - Array выигрывает. tags это O(1), а из Set элемент по номеру не достать. Также если вы работаете с очень маленькими коллекциями (пара десятков элементов), производительность Set не будет видна, а код может быть понятнее с привычным Array.
Для древних браузеров (IE10 и ниже) Set просто не существует - придётся fallback’ить на Array. И если вы делаете какой-то специфический обход или трансформацию данных, Array методы могут быть удобнее.
// Array vs Set: когда Array проще
const tags = ['react', 'vue', 'angular'];
// Нужна трансформация каждого элемента
const tagsByLength = tags.map(tag => tag.length);
// Set это не может делать встроенно
// Нужна фильтрация по условию
const longTags = tags.filter(tag => tag.length > 5);
// Set тоже не может
// Нужен доступ по индексу
const firstTag = tags;
// Set заставит конвертировать в массив
Практический паттерн: обработка тегов поста
Большинство задач с дедупликацией тегов решаются одинаково. Вот универсальный подход, который работает в 90% случаев:
function getUniqueTags(rawTags) {
// rawTags может быть грязным - с пробелами, дубликатами, пустыми строками
return [
...new Set(
rawTags
.map(tag => tag.trim().toLowerCase())
.filter(tag => tag.length > 0)
)
];
}
const userInput = ['React', 'REACT', ' vue ', '', 'Angular', 'react'];
const cleanTags = getUniqueTags(userInput);
console.log(cleanTags);
// ['react', 'vue', 'angular']
Всё чётко: сначала нормализуем данные через map и filter, потом Set отрезает дубликаты, потом разворачиваем обратно в массив.
- Нормализация перед Set: Приводим к одному формату (lowercase, trim), убираем пустоту
- Set деплицирует: Одна операция, всё работает
- Разворот в массив: Если нужен массив дальше, используем spread или Array.from()
- Сохранение порядка: Set гарантирует порядок вставки
На чём не стоит зацикливаться
Есть несколько заблуждений, которые прилипают к Set. Первое - что Set это чудо-решение для всего подряд. Нет, это инструмент для конкретной задачи. Второе - что объекты в Set как-то магически дедублицируются. Нет, они сравниваются по ссылке, и это нормально.
Третье заблуждение - что нужны сложные полифиллы для старых браузеров. Set поддерживается везде, где нужно (IE11+, современные браузеры). Если вы ещё поддерживаете IE10, это вообще отдельная проблема, не только для Set.
И последнее - что производительность Set магична в каждом случае. Set быстрее именно на проверках уникальности и дедупликации больших объёмов. На маленьких массивах разница незаметна.