Выбор структуры данных для уникальных строк в JavaScript

Есть список строк:

'a', 'b', 'a', 'c', 'b'

Какую структуру данных выбрать для хранения списка уникальных строк?

Какую структуру выбрать

Для хранения уникальных строк в JavaScript наиболее подходящей структурой является встроенный Set, потому что он по определению хранит только уникальные значения и предоставляет быстрые проверки наличия через has() и добавление через add().
Для задачи вида ['a', 'b', 'a', 'c', 'b'] → {'a','b','c'} структура Set лучше всего выражает смысл (“множество уникальных значений”) и обычно работает эффективнее, чем подход с Array.prototype.includes() при сопоставимом количестве элементов.

Если в будущем потребуется не просто “уникальность”, а, например, “сколько раз встретилась строка”, структура для решения станет другой (обычно Map или объект-счётчик), потому что понадобится хранить значения-счётчики, а не только факт присутствия.

Теория: что происходит при добавлении

Проверка arr.includes(x) выполняет последовательный просмотр элементов массива до совпадения или конца массива, то есть по смыслу это линейный поиск.
Также includes() сравнивает значения по алгоритму SameValueZero (важно для NaN и для -0/0), но для строк это совпадает с ожидаемым сравнением по содержимому строки.

Типовой алгоритм для уникальности на массиве:

const input = ['a', 'b', 'a', 'c', 'b'];
const unique = [];

for (const s of input) {
  if (!unique.includes(s)) {
    unique.push(s);
  }
}

С точки зрения сложности: при каждом добавлении выполняется includes() по уже накопленному массиву, поэтому при росте данных суммарная работа часто становится квадратичной (примерно O(n^2) по числу элементов).
На малых данных это может быть приемлемо, но при росте массива даже “простая” проверка начинает доминировать по времени.

`Set`: множество уникальных значений

Set хранит значения так, что одно и то же значение не может встречаться в наборе более одного раза, то есть повторное add() не увеличит набор.
Сильная сторона Set — семантика: задача “оставить только уникальные элементы” напрямую соответствует структуре данных “множество”.

Важное поведение, которое часто ожидается:

  • В Set можно итерировать элементы в порядке вставки (то есть в порядке первого появления уникальных элементов).
  • Set.prototype.has() обычно реализуется так, чтобы проверка наличия была эффективной на больших объёмах данных по сравнению с линейным поиском в массиве.

Типовой алгоритм:

const input = ['a', 'b', 'a', 'c', 'b'];
const uniqueSet = new Set(input);

const uniqueArray = [...uniqueSet]; // ['a','b','c']

Объект как “словарь”: ключи-строки → `true`

У обычного объекта ключи — это строки или символы, и это похоже на структуру “словарь”.
Но у объекта есть “унаследованные” свойства (из прототипа), поэтому при использовании пользовательских строк как ключей возможны конфликты с именами вроде __proto__ или constructor, если не предпринять меры.

Чтобы получить объект без прототипа, часто создаётся “чистый словарь” через Object.create(null), и тогда ключи будут только теми, что явно добавлены (это уменьшает риск случайных ключей).

Вариант с обычным объектом (потенциально опаснее из-за прототипа):

const input = ['a', 'b', 'a', 'c', 'b'];
const dict = {}; // есть прототип

for (const s of input) {
  dict[s] = true;
}

Вариант безопаснее для “словаря”:

const input = ['a', 'b', 'a', 'c', 'b'];
const dict = Object.create(null); // без прототипа

for (const s of input) {
  dict[s] = true;
}

const uniqueArray = Object.keys(dict); // порядок ключей — отдельная тема
У объекта есть отличия от Map: Map не содержит ключей “по умолчанию” и безопаснее с пользовательскими ключами, а объект может быть уязвим к нежелательным ключам при неосторожном использовании.

`Map`: ключи → `true`

Map хранит пары ключ-значение, ключи уникальны (один и тот же ключ не может встречаться дважды), а порядок обхода соответствует порядку вставки.
В Map ключом может быть значение любого типа (включая объекты и функции), тогда как у объекта ключи ограничены строками и символами.

Пример “как множество” через Map (обычно избыточно):

const input = ['a', 'b', 'a', 'c', 'b'];
const m = new Map();

for (const s of input) {
  m.set(s, true);
}

const uniqueArray = [...m.keys()]; // ['a','b','c']

Сравнение вариантов

ВариантСемантика (что означает структура)Проверка наличияУникальность “по умолчанию”Подводные камни
1) Array + includes()Список, где уникальность обеспечивается вручнуюincludes() просматривает элементы и сравнивает SameValueZeroНетПри росте данных часто становится медленно из-за повторяющихся линейных проверок
2) SetМножество уникальных значенийhas()Да: значение встречается не более одного разаИногда требуется конвертация в массив, если нужен именно массив
3) Object со значениями true“Словарь” строк → булево значениеПроверка через чтение свойства, например dict[s] === trueНет (уникальность поддерживается логикой записи по ключу)Прототип и “случайные ключи”, безопаснее применять Object.create(null)
4) Map со значениями trueСловарь ключ → значение, ключ уникаленhas(key)Да (ключ встречается один раз)Для задачи “только уникальные строки” обычно избыточен по смыслу, потому что Set выражает задачу точнее

Мини-схема разницы подходов (идея работы):

Вариант 1 (Array):

элемент -> includes(линейный поиск) -> если нет -> push

Вариант 2 (Set):

элемент -> add(встроенная уникальность) -> повтор “игнорируется”

Практические решения

Для получения уникальных строк из массива достаточно const unique = [...new Set(list)], и порядок будет соответствовать первому появлению элементов во входном массиве (то есть сохраняется “порядок первых вхождений”).

const list = ['a', 'b', 'a', 'c', 'b'];
const unique = [...new Set(list)];
// unique: ['a', 'b', 'c']

Если нужен быстрый “словарь” и простой формат (Object.create(null))

Объект может быть удобен, когда требуется сериализация в JSON или максимально простой формат “ключ → признак”, но безопаснее создавать объект без прототипа, чтобы уменьшить риск конфликтов по именам ключей.

const list = ['a', 'b', 'a', 'c', 'b'];

const dict = Object.create(null);
for (const s of list) {
  dict[s] = true;
}

// Проверка наличия
const hasA = dict['a'] === true;

// Если нужны уникальные строки массивом
const unique = Object.keys(dict);
Если требуется хранить произвольные ключи (не только строки) или важна безопасная работа с пользовательскими ключами, то Map обычно предпочтительнее объекта, потому что Map не имеет “случайных” ключей по умолчанию и лучше подходит именно для динамического словаря.

Если потребуется расширение задачи (частоты)

Для подсчёта повторов естественнее перейти с Set на Map, потому что появляется значение-счётчик, а не только факт уникальности.

const list = ['a', 'b', 'a', 'c', 'b'];
const counts = new Map();

for (const s of list) {
  counts.set(s, (counts.get(s) ?? 0) + 1);
}

// counts.get('a') -> 2
// counts.get('b') -> 2
// counts.get('c') -> 1

Итого: для списка уникальных строк наиболее подходящим выбором является Set, потому что он хранит только уникальные значения и обеспечивает эффективные has()/add(); массив с includes() проще, но хуже масштабируется, объект и Map решают похожую задачу, но обычно менее уместны, если нужны только уникальные строки.