Массив-дубликат: почему эта задача собрала тысячи просмотров на Stack Overflow
Представьте: вы пишете интернет-магазин, пользователь добавляет товары в корзину. Но по ошибке или из-за бага один и тот же айфон оказывается в списке трижды. Или вы парсите список тегов из базы данных, а там "JavaScript" встречается 15 раз подряд. Что делать? Правильно — оставить только уникальные значения.
Вопрос "Get all unique values in a JavaScript array (remove duplicates)" на Stack Overflow собрал сотни тысяч просмотров. И это не случайно. Задача кажется простой, но под капотом скрывается целый арсенал подходов: от однострочников на современном ES6 до хитрых трюков с filter и indexOf. В этой статье мы не просто дадим готовые решения, а разберём, как каждое из них работает на уровне движка JavaScript.
Готовы заглянуть под капот? Поехали!
Способ №1: Set — король производительности
Самый элегантный и быстрый способ получить уникальные значения в массиве JavaScript — использовать объект Set. Он появился в ES6 и буквально создан для этой задачи.
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]
Как это работает под капотом?
Объект Set хранит только уникальные значения. Когда мы передаём массив в конструктор new Set(numbers), движок JavaScript проходит по каждому элементу и добавляет его во внутреннюю хеш-таблицу. Если значение уже есть — оно игнорируется. Затем оператор расширения ... превращает Set обратно в массив.
Почему это быстро? Потому что поиск и вставка в Set выполняются за O(1) (константное время). Для массива из 10 000 элементов разница с другими методами будет колоссальной.
Важно: Set сохраняет порядок элементов, но работает только с примитивами (числа, строки, булевы значения). Для объектов сравнение идёт по ссылке, а не по содержимому.
Когда использовать?
- Если массив состоит из чисел или строк.
- Если важна скорость обработки больших массивов.
- Если вы пишете современный код и не боитесь ES6.
Способ №2: filter + indexOf — классика жанра
Этот метод — ветеран JavaScript. Он работает во всех браузерах без полифиллов и до сих пор популярен на Stack Overflow.
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = numbers.filter((value, index, arr) => {
return arr.indexOf(value) === index;
});
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]
Разбор логики
Метод filter создаёт новый массив, оставляя только те элементы, для которых колбэк возвращает true. Внутри колбэка мы проверяем: является ли текущий индекс первым вхождением этого значения в массив? Если да — оставляем элемент. Если значение уже встречалось раньше — indexOf вернёт меньший индекс, условие не выполнится, и дубликат будет отброшен.
Подвох: indexOf каждый раз пробегает по массиву с начала. Это даёт сложность O(n²). Для маленьких массивов (до 1000 элементов) разница незаметна, но на 100 000 элементах браузер может зависнуть на секунду.
Когда использовать?
- Если нужно поддерживать старые браузеры (IE11).
- Если массив небольшой.
- Если вы хотите наглядно показать логику работы "первого вхождения".
Способ №3: reduce + аккумулятор — гибкость и контроль
Метод reduce даёт полный контроль над процессом. Мы сами решаем, как собирать результат.
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = numbers.reduce((acc, value) => {
if (!acc.includes(value)) {
acc.push(value);
}
return acc;
}, []);
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]
Как это работает под капотом?
Мы создаём пустой массив-аккумулятор ([]). На каждой итерации проверяем, есть ли текущее значение в аккумуляторе через includes(). Если нет — добавляем. Если есть — пропускаем.
Нюанс производительности: includes() тоже проходит по массиву, поэтому сложность снова O(n²). Однако этот метод даёт больше возможностей: например, можно добавить логирование, трансформацию данных или сложные условия отбора.
Когда использовать?
- Когда нужно не просто удалить дубликаты, но и преобразовать данные.
- Если вы пишете функциональный код и любите цепочки методов.
- Для учебных целей — наглядно видно, как работает аккумулятор.
Способ №4: forEach + объект-словарь — суперскорость без Set
Хотите скорость, как у Set, но без использования ES6? Используйте объект как хеш-таблицу.
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [];
const seen = {};
numbers.forEach(value => {
if (!seen[value]) {
seen[value] = true;
uniqueNumbers.push(value);
}
});
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]
Разбор механики
Мы создаём пустой объект seen. На каждой итерации проверяем, есть ли ключ с таким значением в объекте. Доступ к свойству объекта — это O(1). Если ключа нет — добавляем его и пушим значение в результирующий массив.
Важное предостережение: Этот метод преобразует все ключи в строки. Число 1 и строка "1" будут считаться одинаковыми. Для смешанных типов данных это может стать проблемой.
Когда использовать?
- Когда нужна максимальная производительность, но Set недоступен.
- Для массивов с примитивами одного типа.
- В легаси-проектах без транспиляции.
Способ №5: Map — для сложных случаев
Если вам нужно удалить дубликаты из массива объектов, сравнивая их по определённому свойству, Map придёт на помощь.
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1,