Объекты Map и Set в JavaScript
Development, Java | Комментировать запись
В 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: Java, Javascript