Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.
В JavaScript разработчики часто тратят много времени на решение того, какую структуру данных следует использовать. Это вызвано тем, что выбор правильной структуры данных упрощает последующее управление этими данными, экономя время и упрощая понимание кода. Двумя преобладающими структурами для хранения коллекций данных являются объекты и массивы (тип объекта). Разработчики используют объекты для хранения пары ключ/значение и массивы для хранения индексированных списков. Однако, чтобы предоставить разработчикам больше гибкости, в спецификации ECMAScript 2015 появились два новых типа итерируемых объектов: карты (map), которые являются упорядоченными коллекциями пар ключ/значение, и сеты (set), которые содержат список уникальных значений.
В этой статье мы поговорим об объектах типов map и set, о том, что их отличает и объединяет с объектами и массивами, о свойствах и методах, доступных для этих типов, а также немного попрактикуемся.
Карты
Map — это коллекция пар ключ/значение, которая может использовать любой тип данных в качестве ключа и поддерживает порядок своих записей. Карты содержат как характеристики объектов (коллекция пар ключ/значение), так и массивов (упорядоченная коллекция), но имеют больше сходства с объектами. Это связано с тем, что хотя размер и порядок записей сохраняется, как и в массиве, сами по себе записи являются парами ключ/значение, как объекты.
Карты можно инициализировать с помощью синтаксиса new Map()
:
const map = new Map()
В результате будет создана пустая карта:
Output
Map(0) {}
Добавление значений в карту
Вы можете добавить значения в карту с помощью метода set()
. Первый аргумент будет ключом, а второй — значением.
Представленный синтаксис добавляет в карту
три пары ключ/значение:
map.set('firstName', 'Luke')
map.set('lastName', 'Skywalker')
map.set('occupation', 'Jedi Knight')
Здесь мы начинаем понимать, что карты имеют черты объектов и массивов. Как и в случае массива, у нас есть индексированная коллекция, и мы можем увидеть количество элементов в карте по умолчанию. Карты используют синтаксис =>
для обозначения пар ключ/значение как key => value
:
Output
Map(3)
0: {"firstName" => "Luke"}
1: {"lastName" => "Skywalker"}
2: {"occupation" => "Jedi Knight"}
Этот пример выглядит аналогично обычному объекту со строковым ключом, но мы можем использовать любой тип данных в качестве ключа в картах.
Помимо ручной настройки значений в карте, мы можем также инициализировать карту с уже добавленными значениями. Мы используем массив массивов, содержащий два элемента, каждый из которых представляет одну пару ключ/значение и выглядит примерно так:
[ [ 'key1', 'value1'], ['key2', 'value2'] ]
Используя следующий синтаксис, мы можем воссоздать ту же самую карту:
const map = new Map([
['firstName', 'Luke'],
['lastName', 'Skywalker'],
['occupation', 'Jedi Knight'],
])
Примечание. Данный пример использует завершающие запятые, также именуемые как подвешенные запятые. Это практика форматирования JavaScript, когда конечный элемент в серии при объявлении коллекции данных имеет запятую в конце. Хотя это форматирование можно использовать для получения более чистых версий кода и облегчения работы с кодом, использовать его или нет решает разработчик. Дополнительную информацию о завершающих запятых см. в статье о завершающих запятых в веб-документации MDN.
Кстати, этот синтаксис выглядит так же, как и результат вызова Object.entries()
для объекта. Это дает готовый способ для преобразования объекта в карту, как показано в следующем блоке кода:
const luke = {
firstName: 'Luke',
lastName: 'Skywalker',
occupation: 'Jedi Knight',
}
const map = new Map(Object.entries(luke))
Также вы можете превратить карту обратно в объект или массив с помощью одной строки кода.
Код ниже преобразует карту в объект:
const obj = Object.fromEntries(map)
В результате будет получено следующее значение obj
:
Output
{firstName: "Luke", lastName: "Skywalker", occupation: "Jedi Knight"}
Теперь мы преобразуем карту в массив:
const arr = Array.from(map)
В результате будет получен следующий массив arr
:
Output
[ ['firstName', 'Luke'],
['lastName', 'Skywalker'],
['occupation', 'Jedi Knight'] ]
Ключи карты
Карты принимают любой тип данных в качестве ключа и не разрешают использование дублирующих ключей. Мы можем продемонстрировать это, создав карту и используя нестроковые значения в качестве ключей, а также задав два значения для одного ключа.
Давайте проинициализируем карту нестроковыми ключами:
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
в качестве уникальных ключей:
Output
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?'
}
В результате вы получите следующее:
Output
{[object Object]: "What will happen?"}
Такой подход с картой не работает. Попробуйте создать объект и задать его как ключ для карты:
// 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?')
Ключом
элемента карты теперь является созданный нами объект.
Output
key: {foo: "bar"}
value: "What will happen?"
Необходимо отметить один важный момент, касающийся использования объекта или массива в качестве ключа: карта использует ссылку на объект для сравнения, а не литеральное значение объекта. В JavaScript {} === {}
возвращает false
, поскольку оба объекта не являются одинаковыми объектами, несмотря на то, что у них одно (пустое) значение.
Это означает, что при добавлении двух уникальных объектов с одинаковым значением будет создана карта с двумя записями:
// Add two unique but similar objects as keys to a Map
map.set({}, 'One')
map.set({}, 'Two')
В результате вы получите следующее:
Output
Map(2) {{…} => "One", {…} => "Two"}
Но при использовании одной ссылки на объект дважды будет создана карта с одной записью.
// Add the same exact object twice as keys to a Map
const obj = {}
map.set(obj, 'One')
map.set(obj, 'Two')
Результат будет выглядеть следующим образом:
Output
Map(1) {{…} => "Two"}
Второй вызов set()
обновляет тот же ключ, что и первый вызов, поэтому в итоге мы получаем карту с одним значением.
Получение и удаление элементов карты
Одним из недостатков работы с объектами является трудность их нумерации или работы со всеми ключами или значениями. Структура карты, напротив, имеет много встроенных свойств, которые делают работу с элементами более понятной.
Мы можем проинициализировать новую карту для демонстрации следующих методов и свойств: delete()
, has()
, get()
и size
.
// Initialize a new Map
const map = new Map([
['animal', 'otter'],
['shape', 'triangle'],
['city', 'New York'],
['country', 'Bulgaria'],
])
Используйте метод has()
для проверки наличия элемента в карте. 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"
Одно конкретное преимущество карт по сравнению с объектами состоит в том, что вы можете получить размер карты в любой момент, как и в случае с массивом. Вы можете получить количество элементов карты с помощью свойства size
. Это позволяет использовать меньше действий по сравнению с преобразованием объекта в массив и получением его длины.
// Get the count of items in a Map
map.size // 4
Используйте метод delete()
, чтобы удалить элемент карты по ключу. Метод будет возвращать булево значение — true
, если элемент существовал и был удален, и false
, если такой элемент не найден.
// Delete an item from a Map by key
map.delete('city') // true
В результате будет получена следующая карта:
Output
Map(3) {"animal" => "otter", "shape" => "triangle", "country" => "Bulgaria"}
Наконец, вы можете удалить все значения карты с помощью map.clear()
.
// Empty a Map
map.clear()
Результат будет выглядеть следующим образом:
Output
Map(0) {}
Ключи, значения и записи для карт
Объекты могут получать ключи, значения и записи, используя свойства конструктора Object
. Карты, напротив, имеют методы, которые позволяют нам получить ключи, значения и записи экземпляра карты.
Методы keys()
, values()
и entries()
возвращают MapIterator
, который похож на массив, где вы можете использовать цикл for...of
для прохождения по всем значениям.
Здесь представлен другой пример карты, который мы можем использовать для демонстрации этих методов:
const map = new Map([
[1970, 'bell bottoms'],
[1980, 'leg warmers'],
[1990, 'flannel'],
])
Метод keys()
возвращает ключи:
map.keys()
Output
MapIterator {1970, 1980, 1990}
Метод values()
возвращает значения:
map.values()
Output
MapIterator {"bell bottoms", "leg warmers", "flannel"}
Метод entries()
возвращает массив пар ключ/значение:
map.entries()
Output
MapIterator {1970 => "bell bottoms", 1980 => "leg warmers", 1990 => "flannel"}
Итерация по карте
Карта имеет встроенный метод forEach
, как и в случае с массивом, для встроенной итерации по карте. Однако есть определенное количество отличий в том, как они выполняют итерацию. Обратный вызов forEach
карты проходит по value
, key
и map
, в то время как версия для массива проходит по item
, index
и array
.
// Map
Map.prototype.forEach((value, key, map) = () => {})
// Array
Array.prototype.forEach((item, index, array) = () => {})
Это огромное преимущество карты над объектом, так как объект нужно преобразовывать для использования keys()
, values()
или entries()
, а простого способа получения свойств объекта без преобразования не существует.
Чтобы продемонстрировать это, давайте пробежимся по нашей карте и выведем пары ключ/значение в консоль:
// Log the keys and values of the Map with forEach
map.forEach((value, key) => {
console.log(`${key}: ${value}`)
})
Это даст нам следующее:
Output
1970: bell bottoms
1980: leg warmers
1990: flannel
Поскольку цикл for...of
поддерживает итерируемые структуры, такие как карты и массивы, мы можем получить аналогичный результат, деструктурируя массив элементов карты:
// 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}`)
}
Свойства и методы карты
В нижеследующей таблице содержится список свойств и методов карты для быстрого просмотра:
Свойства/методы | Описание | Возвращает |
---|---|---|
set(key, value) | Добавляет пару ключ/значение в карту | Карта Объект |
delete(key) | Удаляет пару ключ/значение из карты по ключу | Булево значение |
get(key) | Возвращает значение по ключу | значение |
has(key) | Проверяет наличие элемента в карте по ключу | Булево значение |
clear() | Удаляет все элементы из карты | Нет данных |
keys() | Возвращает все ключи в карте | Объект MapIterator |
values() | Возвращает все значения в карте | Объект MapIterator |
entries() | Возвращает все ключи и значения в карте в виде [key, value] | Объект MapIterator |
forEach() | Выполняет итерацию по карте в порядке добавления | Нет данных |
size | Возвращает количество элементов в карте | Число |
Когда нужно использовать карту
В целом карты напоминают объекты внутри, которые хранят пары ключ/значение, но карты обладают рядом преимуществ над объектами:
- Размер — карты имеют свойство
size
, в то время как объекты не имеют встроенного способа получения размера. - Итерация — карты являются итерируемыми, а объекты нет.
- Гибкость — карты могут содержать любой тип данных (примитивы или объекты) в качестве ключа для значения, а объекты могут хранить только строки.
- Упорядоченность — карты сохраняют порядок добавления, а объекты не имеют гарантированного порядка.
С учетом этих факторов карты являются мощной структурой данных, и вам стоит рассмотреть возможность их использования. Однако объекты также имеют ряд важных преимуществ:
- JSON — объекты отлично работают с
JSON.parse()
иJSON.stringify()
, двумя основными функциями для работы с JSON, стандартным форматом данных, с которым работает множество REST API. - Работа с одним элементом — работая с известным значением в объекте, вы можете получить к нему доступ напрямую с помощью ключа без необходимости использования метода, например метода
get()
карты.
Этот список поможет вам решить, подходит ли в вашем конкретном случае карта или объект.
Сет
Сет — это коллекция уникальных значений. В отличие от карты, сет концептуально больше походит на массив, а не объект, поскольку он представляет собой список значений, а не пар ключ/значение. Однако сет не является заменой массива, он служит дополнением для предоставления новых возможностей работы с дублирующимися данными.
Вы можете инициализировать сеты с помощью синтаксиса new Set()
.
const set = new Set()
В результате будет создан пустой сет:
Output
Set(0) {}
Элементы можно добавить в сет с помощью метода add()
(не путайте с методом set()
, доступным для карты, хотя они и похожи).
// Add items to a Set
set.add('Beethoven')
set.add('Mozart')
set.add('Chopin')
Поскольку сеты могут содержать только уникальные значения, любая попытка добавления уже существующего значения будет игнорироваться.
set.add('Chopin') // Set will still contain 3 unique values
Примечание. Процесс сравнения, используемый для ключей карты, применяется и для элементов сета. Два объекта, имеющие одно и то же значение, но разные ссылки, не считаются равными.
Также вы можете инициализировать сеты с помощью массива значений. Если в массиве есть дублирующие значения, они будут удалены из сета.
// Initialize a Set from an Array
const set = new Set(['Beethoven', 'Mozart', 'Chopin', 'Chopin'])
Output
Set(3) {"Beethoven", "Mozart", "Chopin"}
Аналогично сет можно преобразовывать в массив с помощью одной строки кода:
const arr = [...set]
Output
(3) ["Beethoven", "Mozart", "Chopin"]
Сеты имеют множество общих с картой методов и свойств, включая 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
Обратите внимание, что сет не позволяет получить доступ к значению по ключу или индексу, как, например, Map.get(key)
или arr[index]
.
Ключи, значения и записи для сетов
Карта и сет имеют методы keys()
, values()
и entries()
, которые возвращают итератор. Однако, хотя каждый из этих методов имеет конкретную функцию в карте, у сетов нет ключей, поэтому ключи являются псевдонимами для значений. Это означает, что keys()
и values()
будут возвращать один и тот же итератор, а entries()
будет возвращать значение дважды. В случае с сетом только метод values()
имеет смысл, а два других метода существуют для обеспечения согласованности и кросс-совместимости с картой.
const set = new Set([1, 2, 3])
// Get the values of a set
set.values()
Output
SetIterator {1, 2, 3}
Итерация сета
Как и карта, сет имеет встроенный метод forEach()
. Поскольку у сетов нет ключей, первый и второй параметр обратного вызова forEach()
возвращает одно и то же значение, так что нет необходимости его использования без совместимости с картой. Параметрами forEach()
являются (value, key, 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);
}
В обоих случаях нам потребуется следующее:
Output
hi
hello
good day
Свойства и методы сета
В нижеследующей таблице содержится список свойств и методов сета для быстрого просмотра:
Свойства/методы | Описание | Возвращает |
---|---|---|
add(value) | Добавляет новый элемент в сет | Сет Объект |
delete(value) | Удаляет заданный элемент из сета | Булево значение |
has() | Проверяет наличие элемента в сете | Булево значение |
clear() | Удаляет все элементы из сета | Нет данных |
keys() | Возвращает все значения сета (аналогично values() ) | Объект SetIterator |
values() | Возвращает все значения сета (аналогично keys() ) | Объект SetIterator |
entries() | Возвращает все значения сета в виде [value, value] | Объект SetIterator |
forEach() | Выполняет итерацию по сету в порядке вставки | Нет данных |
size | Возвращает количество элементов сета | Число |
Когда нужно использовать сет
Сет — это полезное дополнение для вашего набора инструментов JavaScript, особенно при работе с дублирующими значениями в данных.
В одной строке мы можем создать новый массив без дублирующих значений из массива, где есть повторяющиеся значения.
const uniqueArray = [ ...new Set([1, 1, 2, 2, 2, 3])] // (3) [1, 2, 3]
Это даст нам следующее:
Output
(3) [1, 2, 3]
Сет можно использовать для поиска идентичных, пересекающихся и отличных элементов двух наборов данных. Однако массивы имеют ряд преимуществ, включая дополнительные инструменты манипуляции данными благодаря методам sort()
, map()
, filter()
и reduce()
, а также прямую совместимость с методами JSON
.
Заключение
Из этой статьи мы узнали, что карта представляет собой коллекцию упорядоченных пар ключ/значение, а сет — это коллекция уникальных значений. Обе эти структуры дают JavaScript дополнительные возможности и упрощают выполнение стандартных задач, например поиск длины коллекции пар ключ/значение и удаление дублирующих элементов из набора данных соответственно. С другой стороны, объекты и массивы традиционно используются для хранения и обработки данных в JavaScript, а также имеют прямую совместимость с JSON, что позволяет им оставаться самыми важными структурами данных, особенно при работе с REST API. Карты и сеты часто используются в качестве вспомогательных структур данных для объектов и массивов.
Если вы хотите узнать больше о JavaScript, перейдите на главную страницу нашей серии материалов Написание кода на JavaScript или перейдите к нашей серии Написание кода на Node.js для изучения материалов по серверной разработке.