Avatar ·

Как удалить дубликаты из массива в JavaScript? Самые эффективные способы

Всем привет! Я новичок в JavaScript и столкнулся с такой задачей: у меня есть массив с числами, в котором много повторяющихся значений. Мне нужно оставить только уникальные элементы. Например, есть массив `[1, 2, 2, 3, 4, 4, 5]`, а на выходе хочу получить `[1, 2, 3, 4, 5]`. Я пробовал написать свою функцию, но она работает неправильно — удаляет не все дубликаты или вообще ломает массив. Вот мой код: ```javascript function removeDuplicates(arr) { for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { if (arr[i] === arr[j]) { arr.splice(j, 1); } } } return arr; } ``` Проблема в том, что после `splice` индексы сдвигаются, и я пропускаю некоторые элементы. Плюс мне кажется, что это решение слишком медленное для больших массивов. Подскажите, пожалуйста, какие есть современные и эффективные способы удаления дубликатов? Интересуют решения как для простых типов (числа, строки), так и для объектов. И хотелось бы понять, какой метод лучше использовать в разных ситуациях. Заранее спасибо! <<>>

1. Использование Set (самый простой и быстрый способ)

Самый элегантный и производительный способ для примитивных типов (числа, строки, булевы значения) — использовать встроенный объект Set. Он автоматически хранит только уникальные значения. Комбинируя его с оператором spread (...) или Array.from(), вы получаете новый массив без дубликатов.

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [...new Set(numbers)];

console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

// Альтернативная запись:
const uniqueNumbers2 = Array.from(new Set(numbers));
console.log(uniqueNumbers2); // [1, 2, 3, 4, 5]

Почему это работает? Объект Set гарантирует, что каждое значение встречается только один раз. При создании new Set(numbers) все дубликаты автоматически отбрасываются. Затем мы преобразуем Set обратно в массив.

Важно: Этот метод работает только для примитивных значений. Для объектов (массивов, словарей) сравнение происходит по ссылке, а не по содержимому, поэтому дубликаты-объекты не будут удалены корректно.

2. Использование filter() и indexOf()

Классический подход, который хорошо подходит для обучения и понимания логики. Метод filter() создает новый массив, а indexOf() возвращает первый индекс найденного элемента. Если текущий индекс элемента совпадает с его первым вхождением — это уникальный элемент.

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = numbers.filter((value, index, self) => {
    return self.indexOf(value) === index;
});

console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

Плюсы: Понятный код, легко читается новичками.
Минусы: Медленнее, чем Set, особенно на больших массивах (сложность O(n²)), так как indexOf() каждый раз проходит по массиву.

3. Использование 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() работает быстрее indexOf() для поиска, но всё равно имеет сложность O(n) на каждой итерации.

4. Удаление дубликатов для объектов (по ключу)

Если у вас массив объектов и нужно удалить дубликаты по конкретному свойству, Set не подойдет. Используйте Map или объект с ключами:

const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 1, name: 'Alice' }, // дубликат по id
    { id: 3, name: 'Charlie' }
];

// Удаляем дубликаты по полю id
const uniqueUsers = Array.from(
    new Map(users.map(user => [user.id, user])).values()
);

console.log(uniqueUsers);
// Результат: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]

Объяснение: Метод Map использует id как ключ. Если встречается дубликат, он перезаписывает предыдущее значение. Затем мы берем только значения (.values()) и преобразуем в массив.

Сравнение производительности (для массива из 100 000 элементов)

  • Set: ~1-3 мс — самый быстрый
  • filter + indexOf: ~2000-5000 мс — очень медленный
  • reduce + includes: ~100-300 мс — средний
  • Map для объектов: ~5-10 мс — быстрый

Для 99% случаев используйте Set — это современный стандарт. Если нужно сохранить порядок или работать с объектами — используйте Map или reduce.

<<>>

Альтернативный подход: Lodash и однострочники

Если вы используете библиотеку Lodash (часто встречается в крупных проектах), задача решается одной функцией:

const _ = require('lodash');
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = _.uniq(numbers);
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

// Для объектов по ключу:
const users = [{id:1}, {id:2}, {id:1}];
const uniqueUsers = _.uniqBy(users, 'id');
console.log(uniqueUsers); // [{id:1}, {id:2}]

Плюсы: Кратко, читаемо, протестировано тысячами разработчиков. Lodash также оптимизирован для производительности.

Минусы: Добавляет зависимость (около 70 КБ в сжатом виде). Для простых задач это может быть избыточно.

Исправление вашего первоначального кода

Ваш код с вложенными циклами и splice имеет ошибку сдвига индексов. Вот исправленная версия:

function removeDuplicates(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1);
                j--; // Корректируем индекс после удаления
            }
        }
    }
    return arr;
}

Добавление j-- решает проблему, но такой подход всё равно медленный (O(n²)) и мутирует исходный массив. Лучше использовать неизменяемые методы, которые возвращают новый массив.

Для очень больших массивов (миллионы элементов)

Если производительность критична, рассмотрите использование TypedArray или Web Workers. Но для 99% задач Set или _.uniq() будет достаточно.