JavaScript Set и Map: За пределами массивов и объектов

Источник: «JavaScript Sets and Maps: Beyond Arrays and Objects»
Как эффективно обрабатывать уникальные значения и пары ключ-значение, избегая принудительного приведения типов и потери производительности?

До того как в ES6 появились Set и Map, возможности хранения коллекций данных в JavaScript были ограничены. Использовались объекты для пар ключ-значение и массивы для списков. Это приводило к распространённым проблемам:

// Проблема 1: Использование массивов для уникальных значений
const userIds = [1, 2, 2, 3]
const uniqueIds = userIds.filter((id, index) =>
userIds.indexOf(id) === index
)
console.log(uniqueIds)
// [1, 2, 3] ✅ Правильно, но неэффективно!
// Приходится многократно просматривать массив

// Проблема 2: Коллизии чисел и строк в качестве ключей
const cache = {}
cache[123] = "data" // Использование числа в качестве ключа
cache["123"] = "other data" // Использование строки в качестве ключа
console.log(cache[123])// "other data" - оба ключа преобразуются в строку "123"

// Проблема 3: Объекты в качестве ключей не работают
const userMap = {}
const user = {id: 1}
userMap[user] = "data"
console.log(userMap[user]) // "data"
console.log(userMap["[object Object]"])// "data" - то же значение!

// Оба объекта становятся "[object Object]".
const user2 = {id: 2}
userMap[user2] = "different data"
console.log(userMap[user])// "different data" - первое значение было перезаписано!

Что такое Set и Map

Set и Map — специализированные структуры данных в JavaScript, каждая предназначена для решения специфических задач, с которыми плохо справляются массивы и объекты.

Set — коллекция уникальных значений. Думайте о нём как о сумке, автоматически удаляющей дубликаты.

При добавлении одного и того же значения в Set дважды сохраняется только одна копия. Он прекрасно подходит для создания списков, в которых каждый элемент должен встречаться только один раз.

// Set автоматически обрабатывает уникальность
const uniqueNumbers = new Set([1, 1, 2, 2, 3])
console.log(uniqueNumbers) // ✅ Set(3) {1, 2, 3}

// Быстрый поиск при проверке существования
uniqueNumbers.has(1) // true
uniqueNumbers.has(4) // false

// Простое добавление и удаление элементов
uniqueNumbers.add(4) // Set(4) {1, 2, 3, 4}
uniqueNumbers.delete(1) // Set(3) {2, 3, 4}

Map — коллекция пар ключ-значение, где ключи могут быть любого типа — числа, строки, объекты, даже функции. В отличие от объектов, преобразующих все ключи в строки, Map сохраняет тип ключа. Это делает их идеальными для создания словарей или кэшей, где тип ключа имеет значение:

// Map поддерживает типы ключей
const userProfiles = new Map()

// Числа остаются числами
userProfiles.set(123, "John")
userProfiles.set("123", "Jane") // Отличается от 123

// Объекты могут быть ключами
const user = {id: 1}
userProfiles.set(user, "User data")

console.log(userProfiles.get(123)) // "John"
console.log(userProfiles.get("123")) // "Jane"
console.log(userProfiles.get(user)) // "User data"

Так, Set и Map решают две проблемы, рассмотренные ранее при работе с массивами и объектами.

// Set для уникальных значений
const userIds = new Set([1, 2, 2, 3]) // Автоматическая уникальность
console.log(userIds) // Set(3) {1, 2, 3}

// Map для корректного хранения ключ-значение
const cache = new Map()
cache.set(123, "data") // Числовой ключ
cache.set("123", "other") // Строковый ключ - другой!

const userMap = new Map()
const user = {id: 1}
userMap.set(user, "data") // Объект в качестве ключа - работает!

Set и Map отлично справляются с отношениями данных, кэшированием и проверкой уникальности. У каждого из них есть определённые сценарии использования, позволяющие превзойти традиционные массивы и объекты.

Когда использовать Set

Используйте Set, если требуется быстрый поиск и гарантия уникальности данных. В системе тегов можно мгновенно проверить, есть ли в статье определённый тег, не перебирая массив.

// Быстрая проверка существования
const adminPermissions = new Set(['edit', 'delete', 'create'])
if (adminPermissions.has('delete')) { // Мгновенный поиск
deleteResource()
}

// Отслеживание уникальных значений
const uniqueVisitors = new Set()
uniqueVisitors.add(userId) // Автоматическая обработка дубликатов

Когда использовать Map

Map — идеальный вариант, если нужно связать данные с любым типом ключа — например, кэшировать ответы API по URL, сохранять предпочтения пользователей или поддерживать связь между элементами DOM и их данными.

// Типобезопасные ключи
const apiCache = new Map()
apiCache.set('/api/users', userData) // URL в виде точной строки
apiCache.set(404, errorData) // Число остаётся числом

// Objects as keys
const elementStates = new Map()
const button = document.querySelector('#submit')
elementStates.set(button, { // DOM элемент в качестве ключа
clicks: 0,
lastClicked: null
})

Компромиссы производительности

Set и Map обеспечивают O(1) операций благодаря реализации хэш-таблиц, по сравнению с массивами, имеющие O(n) для поиска, и объектами, преобразующими ключи в строки.

Однако за эту скорость приходится платить: Set и Map используют больше памяти, чем массивы и объекты. Структура хэш-таблицы, обеспечивающая их быстрые операции, требует дополнительных затрат памяти на обслуживание.

В частности, для Set хэш-таблица обеспечивает мгновенное обнаружение дубликатов, а не требует полного сканирования массива. Map достигают своей производительности за счёт использования аналогичной структуры, но хранят и ключ, и значение в хэш-таблице.

// Производительность Set
const set = new Set()
set.add(value) // O(1) - постоянное время вычисления хэша
set.has(value) // O(1) - прямой поиск по хэш-таблице
set.delete(value) // O(1) - немедленное удаление из хэш-таблицы
set.size // O(1) - поддерживается счётчик размеров

// Производительность Map
const map = new Map()
map.set(key, value) // O(1) - вставка в хэш-таблицу
map.get(key) // O(1) - прямой поиск по хэшу
map.has(key) // O(1) - проверка существования в хэше
map.delete(key) // O(1) - удаление из хэш-таблицы
map.size // O(1) - постоянное отслеживание размера

Set и Map отлично справляются с пользовательскими сессиями. Set обеспечивает мгновенный поиск O(1), проверяющий, вошёл ли пользователь в систему, что гораздо быстрее, чем поиск по массиву пользовательских ID. Map позволяет хранить данные сессий с любым типом идентификатора пользователя (число, строка или объект) без проблем с преобразованием типов ключей.

class SessionManager {
constructor() {
this.activeSessions = new Map() // userId -> данные сессии
this.loggedInUsers = new Set() // уникальные ID пользователей
}

login(userId, sessionData) {
this.loggedInUsers.add(userId) // O(1) добавление
this.activeSessions.set(userId, {
data: sessionData,
startTime: Date.now()
})
}

isLoggedIn(userId) {
return this.loggedInUsers.has(userId) // O(1) проверка
}

getSessionData(userId) {
return this.activeSessions.get(userId) // O(1) поиск
}
}

Пример SessionManager демонстрирует, как Set может отслеживать уникальных активных пользователей, в то время как Map хранит подробные данные о сеансе. Эта комбинация эффективна, поскольку разделяет задачи: Set обрабатывает уникальность, а Map — ассоциацию данных.

Мгновенное время поиска делает эту комбинацию идеальной для приложений с высокой посещаемостью, где производительность имеет решающее значение.

Когда не стоит использовать Set и Map

Set и Map требуют больших затрат памяти из-за структуры хэш-таблиц. Для небольших коллекций или простых строковых ключей более эффективными могут быть массивы и объекты.

// 1. Нужны методы массива
const numbers = [1, 2, 3, 4]
numbers.map(n => n * 2) // Это невозможно сделать с Set
numbers.filter(n => n > 2) // В Set нет встроенной фильтрации
numbers.reduce((a, b) => a + b) // В Set нет уменьшения

// 2. Нужен доступ к индексу
const items = ['first', 'second', 'third']
console.log(items[0]) // Прямой доступ к индексу
console.log(items.indexOf('second')) // Позиция важна

// 3. Нужна сериализация JSON
const user = {name: 'John', age: 30}
JSON.stringify(user) // Работает отлично
JSON.stringify(new Map()) // Пустой объект {}
JSON.stringify(new Set()) // Пустой массив []

// 4. Нужны простые только строковые ключи
// Использование Map будет излишним
const config = {
apiKey: '123',
baseUrl: 'api.example.com'
}

Set и Map — не просто альтернатива массивам и объектам, это специализированные инструменты, делающие конкретные паттерны программирования более эффективными и надёжными. Используйте их, когда уникальные характеристики соответствуют потребностям, и вы будете писать более надёжный и производительный код.

Комментарии


Дополнительные материалы

Предыдущая Статья

Кнопки с несколькими состояниями

Следующая Статья

CSS :has() — Псевдокласс, который давно ждали