Объекты Map и Set в JavaScript

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

Читайте также:

Разработчики используют объекты для хранения пар «ключ-значение», а массивы – для хранения индексированных списков. Чтобы предоставить разработчикам больше гибкости, JavaScript представил два новых типа итерируемых объектов в спецификации ECMAScript 2015: Maps (упорядоченные коллекции пар «ключ-значение») и Sets (коллекции уникальных значений).

В этой статье мы расскажем вам об объектах Map и Set, их схожестях и отличиях от объектов и массивов, о доступных для них свойствах и методах, а также приведем примеры на практике.

Объекты Map

Объект Map – это набор пар «ключ-значение», который может поддерживать порядок записей. В качестве ключа может использоваться любой тип данных. Объекты Map имеют элементы и объектов (уникальная коллекция пар «ключ-значение»), и массивов (упорядоченная коллекция), но концептуально они все же больше похожи на объекты. Это связано с тем, что сами записи являются подобными объектам парами «ключ-значение», хотя размер и порядок записей сохраняются в виде массива.

Объекты Map можно инициализировать с помощью синтаксиса new Map():

const map = new Map()

Это создаст пустой объект Map:

Map(0) {}

Добавление значений в объект Map

Вы можете добавить значения в объект Map с помощью метода set(). Первый аргумент будет ключом, а второй – значением.

Следующий синтаксис добавляет три пары «ключ-значение»:

map.set('firstName', 'Luke')
map.set('lastName', 'Skywalker')
map.set('occupation', 'Jedi Knight')

Здесь вы можете увидеть, что у Map есть элементы как объектов, так и массивов. Подобно массиву, Map – это коллекция с нулевым индексом. Количество элементов в Map видно по умолчанию. Объекты Map используют синтаксис => для обозначения пар «ключ-значение»: key => value.

Map(3)
0: {"firstName" => "Luke"}
1: {"lastName" => "Skywalker"}
2: {"occupation" => "Jedi Knight"}

Этот пример похож на обычный объект со строковыми ключами, но в целом в качестве ключа в Maps можно использовать любой тип данных.

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

[ [ 'key1', 'value1'], ['key2', 'value2'] ]

Используя следующий синтаксис, мы можем создать такой же Map:

const map = new Map([
['firstName', 'Luke'],
['lastName', 'Skywalker'],
['occupation', 'Jedi Knight'],
])

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

К слову, этот синтаксис совпадает с результатом вызова Object.entries() для объекта. Это готовый способ преобразования объекта в Map, как показано в следующем блоке кода:

const luke = {
firstName: 'Luke',
lastName: 'Skywalker',
occupation: 'Jedi Knight',
}
const map = new Map(Object.entries(luke))

Также вы можете преобразовать Map обратно в объект или массив. Для этого нужна всего одна строка кода. Чтобы преобразовать в объект:

const obj = Object.fromEntries(map)

В результате получится следующее значение obj:

{firstName: "Luke", lastName: "Skywalker", occupation: "Jedi Knight"}

Чтобы преобразовать Map в массив:

const arr = Array.from(map)

В результате получится такое значение arr:

[ ['firstName', 'Luke'],
['lastName', 'Skywalker'],
['occupation', 'Jedi Knight'] ]

Ключи Map

Объекты Map принимают любой тип данных в качестве ключа, но не допускают дублирования ключей. Давайте попробуем Map и использовать в качестве ключей не строковые значения, а числа. Также для примера мы установим здесь два одинаковых ключа.

Давайте для начала инициализируем Map с нестроковыми ключами:

const map = new Map()
map.set('1', 'String one')
map.set(1, 'This will be overwritten')
map.set(1, 'Number one')
map.set(true, 'A Boolean')

Этот пример переопределит первый ключ 1 с помощью второго ключа, но он  будет обрабатывать строку ‘1’ и число 1 как уникальные ключи (поскольку это разные типы):

0: {"1" => "String one"}
1: {1 => "Number one"}
2: {true => "A Boolean"}

Хотя распространено мнение, что обычный объект JavaScript уже может обрабатывать числа, логические значения и другие примитивные типы данных в качестве ключей, на самом деле это не так – объекты заменяют все ключи на строки.

Для примера инициализируйте объект с числовым ключом, а затем сравните значения числового ключа 1 и строкового ключа «1»:

// Initialize an object with a numerical key
const obj = { 1: 'One' }
// The key is actually a string
obj[1] === obj['1']  // true

Вот почему, если вы попытаетесь использовать объект как ключ, на экране появится строка object Object.

А теперь создайте объект и используйте его в качестве ключа другого объекта:

// Create an object
const objAsKey = { foo: 'bar' }
// Use this object as the key of another object
const obj = {
[objAsKey]: 'What will happen?'
}

Вы получите:

{[object Object]: "What will happen?"}

Объекты Map работают не так. Попробуйте создать объект и установить его в качестве ключа Map:

// Create an object
const objAsKey = { foo: 'bar' }
const map = new Map()
// Set this object as the key of a Map
map.set(objAsKey, 'What will happen?')

Ключ элемента Map – это объект, который мы создали.

key: {foo: "bar"}
value: "What will happen?"

Следует обратить внимание на одну важную вещь, касающуюся использования объекта или массива в качестве ключа: для сравнения равенства Map использует ссылку на объект, а не буквальное значение объекта. В JavaScript {} === {} возвращает значение false, поскольку два объекта не являются одинаковыми двумя объектами, несмотря на одинаковое (пустое) значение.

Это означает, что добавление двух уникальных объектов с одинаковым значением создаст Map с двумя записями:

// Add two unique but similar objects as keys to a Map
map.set({}, 'One')
map.set({}, 'Two')

Это приведет к следующему:

Map(2) {{…} => "One", {…} => "Two"}

Но использование одной и той же ссылки на объект дважды приведет к созданию Map с одной записью.

// Add the same exact object twice as keys to a Map
const obj = {}
map.set(obj, 'One')
map.set(obj, 'Two')

Что приведет к следующему:

Map(1) {{…} => "Two"}

Второй метод set() обновляет тот же самый ключ, что и первый, поэтому мы получаем Map, который имеет только одно значение.

Извлечение и удаление элементов из Map

Один из недостатков работы с объектами – бывает сложно вычислить их ключи или работать со всеми ключами и  значениями. В отличие от объектов, структура Map имеет много встроенных свойств, которые позволяют работать непосредственно с ее элементами.

Мы можем инициализировать новый Map, чтобы продемонстрировать методы и свойства delete(), has(), get() и size.

// Initialize a new Map
const map = new Map([
['animal', 'otter'],
['shape', 'triangle'],
['city', 'New York'],
['country', 'Bulgaria'],
])

Используйте метод has(), чтобы проверить наличие элемента в Map. Метод has() вернет логическое значение.

// Check if a key exists in a Map
map.has('shark') // false
map.has('country') // true

Используйте метод get(), чтобы извлечь значение по ключу.

// Get an item from a Map
map.get('animal') // "otter"

Одно из особых преимуществ структур Map по сравнению с объектами заключается в том, что вы можете легко выяснить размер Map (как это можно сделать и с массивом). Вы можете получить количество элементов в Map с помощью свойства size. Это работает гораздо быстрее, чем преобразование объекта в массив для определения их длины.

// Get the count of items in a Map
map.size // 4

Используйте метод delete(), чтобы удалить элемент из Map по ключу. Метод вернет логическое значение – true, если элемент существовал и был удален, и false, если заданный ключ не соответствует ни одному элементу.

// Delete an item from a Map by key
map.delete('city') // true

В результате получится следующий Map:

Map(3) {"animal" => "otter", "shape" => "triangle", "country" => "Bulgaria"}

Наконец, Map можно очистить от всех значений с помощью map.clear().

// Empty a Map
map.clear()

Получится такой вывод:

Map(0) {}

Ключи, значения и записи для Map

Объекты могут получать ключи, значения и записи, используя свойства конструктора Object. В свою очередь структуры Map имеют методы-прототипы, которые позволяют напрямую извлекать ключи, значения и записи экземпляра Map.

Методы keys(), values() и entries() возвращают MapIterator, который аналогичен массиву, где можно использовать цикл for…of  для итерации значений.

Вот пример Map, в котором демонстрируются эти методы:

const map = new Map([
[1970, 'bell bottoms'],
[1980, 'leg warmers'],
[1990, 'flannel'],
])

Метод keys() вернет ключи:

map.keys()
MapIterator {1970, 1980, 1990}

Метод values() вернет значения:

map.values()
MapIterator {"bell bottoms", "leg warmers", "flannel"}

Метод entries() вернет массив пар ключ-значение:

map.entries()
MapIterator {1970 => "bell bottoms", 1980 => "leg warmers", 1990 => "flannel"}

Итерация структур Map

В Map есть встроенный метод forEach для итерации. Подобный метод есть в массивах. Однако в их работе есть небольшая разница. Обратный вызов forEach объекта Map выполняет итерацию по значению, ключу и самому Map, а в массиве – по элементу, индексу и самому массиву.

// Map
Map.prototype.forEach((value, key, map) = () => {})
// Array
Array.prototype.forEach((item, index, array) = () => {})

Это большое преимущество Map над объектами, поскольку объекты нужно преобразовывать с помощью keys(), values(), or entries(). Получить свойства объекта без его преобразования невозможно.

Чтобы продемонстрировать это, давайте выполним итерацию по Map и выведем пары ключ-значение на консоль:

// Log the keys and values of the Map with forEach
map.forEach((value, key) => {
console.log(`${key}: ${value}`)
})

Получится такой результат:

1970: bell bottoms
1980: leg warmers
1990: flannel

Поскольку цикл for…of выполняет итерации по таким элементам, как Map и массив, мы можем получить точно такой же результат, деструктурировав массив элементов Map:

// Destructure the key and value out of the Map item
for (const [key, value] of map) {
// Log the keys and values of the Map with for...of
console.log(`${key}: ${value}`)
}

Свойства и методы Map

В следующей таблице приведен список свойств и методов Map для быстрого ознакомления с ними:

Свойства и методы Описание Вывод
set(key, value) Добавляет пару ключ-значение в Map Объект Map
delete(key) Удаляет пару ключ-значение из Map по ключу Логическое значение
get(key) Возвращает значение по ключу Значение
has(key) Проверяет наличие элемента в Map по ключу Логическое значение
clear() Удаляет все элементы из Map N/A
keys() Возвращает все ключи в Map Объект MapIterator
values() Возвращает все значения в Map Объект MapIterator
entries() Возвращает все ключи и значения в Map в формате [ключ, значение] Объект MapIterator
forEach() Итерация по Map в порядке вставки N/A
size Возвращает количество элементов в Map Число

Использование структур Map

Подводя итог, отметим еще раз, что Map похожи на объекты тем, что они тоже состоят из пар ключ-значение. Но Map имеют несколько преимуществ перед объектами:

  • Размер: у Map есть свойство size, тогда как у объектов нет встроенного способа вычислить их размер.
  • Итерация: структуры Map являются итеративными, а объекты – нет.
  • Гибкость: в качестве ключей к значениям Map поддерживают разные типы данных (примитивные или объекты), в то время как объекты могут использовать только строки.
  • Упорядоченность: Map сохраняют элементы в порядке вставки, тогда как объекты являются неупорядоченными.

Благодаря этим преимуществам Map являются мощной структурой данных. Тем не менее, объект имеет ряд важных преимуществ над Map:

  • JSON: объекты безупречно работают с функциями JSON.parse() и JSON.stringify(). Это важно потому, что JSON – очень распространенный формат данных, с которым работают многие REST API.
  • Работа с одним элементом: работая с известным значением в объекте, вы можете обращаться к нему напрямую с помощью ключа без необходимости использования метода типа get().

Зная об этих особенностях, вы можете принять взвешенное решение о структуре данных для вашего проекта.

Объекты Set

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

Для инициализации Set используется синтаксис new Set():

const set = new Set()
Set(0) {}

Элементы можно добавить в Set с помощью метода add(). Не следует путать его с методом set(), который есть в Map, (хотя они похожи).

// Add items to a Set
set.add('Beethoven')
set.add('Mozart')
set.add('Chopin')

Поскольку Set может содержать только уникальные значения, любая попытка добавить уже существующее значение будет проигнорирована.

set.add('Chopin') // Set will still contain 3 unique values

Примечание: То же сравнение равенства, которое применяется к ключам Map, относится и к элементам Set. Два объекта, которые имеют одинаковое значение, но разные ссылки, не будут считаться равными.

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

// Initialize a Set from an Array
const set = new Set(['Beethoven', 'Mozart', 'Chopin', 'Chopin'])
Set(3) {"Beethoven", "Mozart", "Chopin"}

И наоборот, Set можно преобразовать в массив с помощью всего одной строки кода:

const arr = [...set]
(3) ["Beethoven", "Mozart", "Chopin"]

Set поддерживает много тех же методов и свойств, что и Map, включая delete(), has(), clear() и size.

// Delete an item
set.delete('Beethoven') // true
// Check for the existence of an item
set.has('Beethoven') // false
// Clear a Set
set.clear()
// Check the size of a Set
set.size // 0

Обратите внимание, Set не предоставляет доступа к значению по ключу или индексу, как Map.get(key) или arr[index].

Ключи, значения и записи Set

У Map и Set есть методы keys(), values​​() и entries(), которые возвращают Iterator. В Map каждый из этих методов имеет отдельное назначение, но Set не имеет ключей, и поэтому ключи являются псевдонимами для значений. Это означает, что keys() и values​​() будут возвращать один и тот же Iterator, а entries() будет возвращать значение дважды. Лучше всего использовать в Set только values​​(), так как два других метода существуют для согласованности и перекрестной совместимости с Map.

const set = new Set([1, 2, 3])
// Get the values of a set
set.values()
SetIterator {1, 2, 3}

Итерация Set

Как и Map, Set имеет встроенный метод forEach(). Поскольку Set не имеет ключей, первый и второй параметр обратного вызова forEach() возвращают одно и то же значение. Поэтому Set нельзя использовать вне совместимости с Map. Параметры forEach() – это (value, key, set).

В Set можно использовать как forEach(), так и for…of. Давайте посмотрим на итерацию forEach():

const set = new Set(['hi', 'hello', 'good day'])
// Iterate a Set with forEach
set.forEach((value) => console.log(value))

А теперь можно взглянуть на версию for…of:

// Iterate a Set with for...of
for (const value of set) {
console.log(value);
}

Обе эти стратегии вернут такой результат:

hi
hello
good day

Свойства и методы Set

В следующей таблице вы найдете список свойств и методов Set для быстрого ознакомления:

Свойства и методы Описание Вывод
add(value) Добавляет в Set новый элемент Объект Set
delete(value) Удаляет указанный элемент из Set Логическое значение
has() Проверяет наличие элемента в Set Логическое значение
clear() Удаляет все элементы из Set N/A
keys() Возвращает все значения в Set (так же как values()) Объект SetIterator
values() Возвращает все значения в Set (так же, как keys()) Объект SetIterator
entries() Возвращает все значения в Set в формате [значение, значение] Объект SetIterator
forEach() Перебирает Set в порядке вставки N/A
size Возвращает количество элементов в Set Число

Использование структуры Set

Структура Set является полезным дополнением к инструментарию JavaScript, особенно для работы с дублирующимися значениями в данных.

С помощью одной строки мы можем создать новый массив без повторяющихся значений на основе массива, который имеет повторяющиеся значения.

const uniqueArray = [ ...new Set([1, 1, 2, 2, 2, 3])] // (3) [1, 2, 3]
(3) [1, 2, 3]

Set может помочь вам определить пересечения и разницу между двумя наборами данных. Однако массивы имеют значительное преимущество перед Set, оно заключается в дополнительной обработке данных через методы sort(), map(), filter() и reduce(), а также в прямой совместимости с методами JSON.

Заключение

Теперь вы знаете, что Map – это набор упорядоченных пар ключ-значение, а Set – набор уникальных значений. Обе эти структуры данных предлагают дополнительные возможности в JavaScript и упрощают общие задачи: определение длины коллекции пар ключ-значение и удаление дублирующихся элементов из набора данных. С другой стороны, объекты и массивы традиционно используются для хранения и обработки данных в JavaScript и имеют прямую совместимость с JSON, что делает их наиболее важными структурами, особенно для работы с REST API-интерфейсами. Map и Set в первую очередь полезны в качестве вспомогательных структур для объектов и массивов.

Tags: ,