Валерий Шестернин
JavaScript предлагает несколько встроенных структур данных, которые облегчают работу с коллекциями. В этой статье мы рассмотрим два таких инструмента - Map и Set, их преимущества, особенности использования, а также примеры практического применения.
Отличие Map от Object
Map - это вид объекта JavaScript, но в отличии от обычных объектов, ключи в Map могут быть любым типом данных, а не только строками или символами. Однако важно понимать, что для корректной работы с этой структурой данных нужно использовать специальные методы и свойства:
new Map()
– создаёт коллекцию принимая массив или другой итерируемый объект (const myMap = new Map([[1, “a”], [2, “b”]]
)..set(key, value)
– записывает по ключуkey
значениеvalue
при этом возвращая ту же коллекцию, поэтому можно строить цепочки (myMap.set(1, "a").set(2, "b")
)..size
– возвращает текущее количество пар ключ-значение. Это количество нельзя переопределить какlength
у массива..get(key)
– возвращает значение по ключу илиundefined
, если ключkey
отсутствует..delete(key)
– удаляет пару ключ-значение по ключуkey
и возвращаетtrue
если ключkey
был в коллекции, в противном случае прокидывает ошибку..clear()
– полностью очищает коллекцию от всех пар ключ-значение ..has(key)
– проверяет наличие ключаkey
в коллекции и возвращает ответ в виде булева значения (true
если ключ найден иfalse
если нет)..keys()
– возвращает итерируемый объект по ключам..values()
– возвращает итерируемый объект по значениям..entries()
– возвращает итерируемый объект по парам вида[key, value]
, этот вариант используется по умолчанию вfor..of
..forEach
- метод для перебора Map, схожий со встроенным методом массивов.
jsx
- метод для перебора Map, схожий со встроенным методом массивов.
const john = { name: "John" }; const kate = { name: "Kate" };
const usersAge = {}; //создаем пустой объект usersAge[john] = 25; usersAge[kate] = 20; //присваиваем ключами объекты console.log(usersAge[john]); //20 console.log(usersAge); //{ '[object Object]': 20 } //несмотря на то что для john мы указали возраст 25 по ключу хранится значение 20. const usersAgeWithMap = new Map(); usersAge[john] = 25; usersAge[kate] = 20; //делаем то же самое с Map console.log(usersAgeWithMap[john]); //undefined console.log(usersAgeWithMap); //Map(0) {} //А теперь повторим действия, но с использованием методов set и get usersAgeWithMap.set(john, 25).set(kate, 20); console.log(usersAgeWithMap.get(john)); //25 console.log(usersAgeWithMap); //Map(2) { { name: 'John' } => 25, { name: 'Kate' } => 20 } usersAgeWithMap.size = 1; console.log(usersAgeWithMap); //Map(2) { { name: 'John' } => 25, { name: 'Kate' } => 20 } console.log(usersAgeWithMap.clear()); //Map(2) { { name: 'John' } => 25, { name: 'Kate' } => 20 }
На примере выше мы видим, как при попытке присвоить обычному объекту ключи в виде других объектов, значение перезаписывается, а не создается новое свойство. Это происходит потому что объекты, которые мы передали как ключи, преобразуются в строку и строковое выражение обоих объектов john и kate равно друг другу(*`'[object Object]'`*). При попытке добавить в Map пары ключ-значение через прямое обращение без метода `set` (`map[key]=value`) эти пары не добавляются. Все начинает работать как задумано только при использовании специальных методов. Такое ограничение синтаксиса может показаться неудобным, ведь если мы не знаем в каком формате (Map и Object) к нам приходят данные, придется писать проверку и работать с ними по разному, но, из-за такой чувствительности к применению методов, Map становится безопаснее, ведь нельзя переназначить свойства по-умолчанию (как `length` у массива) и изменить прототип коллекции.
Map (как и Set) использует алгоритм SameValueZero для сравнения ключей. Этот алгоритм практически идентичен оператору строгого сравнения (`===`), за исключением того, что NaN считается равным NaN. Поэтому, NaN может быть использован в качестве ключа в Map. Важно отметить, что этот алгоритм не может быть изменен или модифицирован.
В отличии от объектов, Map хранит пары ключ-значение в том порядке, в котором они были добавлены, что может быть полезно при переборе.
```jsx
const myObj = {};
myObj["01"] = 1;
myObj["a"] = 2;
myObj[1] = 3;
console.log(myObj); //{ '1': 3, '01': 1, a: 2 }
const myMap = new Map();
myMap.set("01", 1).set("a", 2).set(1, 3);
console.log(myMap); //Map(3) { '01' => 1, 'a' => 2, 1 => 3 }
Есть у Map и серьезные недостатки. У этой структуры данных нет встроенной поддержки сериализации или синтаксического анализа. Т.е. мы не можем просто передать Map в JSON.stringify()
что бы получить строку со всеми данными или распарсить JSON и получить полную структуру Map. Придется писать свою реализацию.
const myMap = new Map([
["name", "Kate"],
["age", 20],
]);
const stringFromMap = JSON.stringify(myMap); //строка: {}
//Map не выйдет просто так преобразовать в строку
const objFromMap = Object.fromEntries(myMap.entries());
//преобразуем наш Map в Object
const stringFromMapWithObj = JSON.stringify(objFromMap); //строка: {"name":"Kate","age":20}
//объект уже легко преобразуется в строку
const objFromString = JSON.parse(stringFromMapWithObj); //объект: { name: 'Kate', age: 20 }
const myNewMap = new Map(Object.entries(objFromString)); //Map(2) { 'name' => 'Kate', 'age' => 20 }
//парсить данные формата JSON в Map так же придется через преобразование в объект
В примере выше мы преобразуем Map в простые пары ключ-значение с помощью метода entries
и передаем результат в метод fromEnteries
глобального объекта Object что бы потом с помощью JSON.stringify
преобразовать данные в строку. Что бы распарсить JSON в Map мы проводим обратные операции. Если такие конструкции кажутся слишком громоздкими, представьте, если бы ключом был объект, ведь при попытке собрать Object из такой пары наш ключ превратится в строку '[object Object]'
.
Применение Map
Структура данных Map в JavaScript предоставляет множество возможностей и находит широкое применение в различных случаях. Вот лишь несколько примеров:
- Частое применение Map - это сохранение связи между объектами и другими значениями. Например, если мы хотим хранить метаданные отдельно от самой сущности.
const product1 = { id: 1, name: "coat", price: 100 };
const product2 = { id: 2, name: "t-shirt", price: 50 };
const productsMetaData = new Map();
productsMetaData.set(product1, { creator: "John", createdAt: new Date() });
productsMetaData.set(product2, { creator: "Kate", createdAt: new Date() });
console.log(productsMetaData.get(product1));
//{ creator: 'John', createdAt: <дата и время создания> }
- Если Вам нужно очень часто добавлять и удалять пары ключ-значение Map хорошо оптимизирован для подобных операций и справляется в несколько раз быстрее, чем Object.
const myObj = {};
for (let i = 0; i <= 10000000; i++) {
myObj[i] = i;
delete myObj[i];
} //выполнится за 1325 миллисекунд
const myMap = new Map();
for (let i = 0; i <= 10000000; i++) {
myMap.set(i, i);
myMap.delete(i);
} //выполнится за 378 миллисекунд
- Даже если в Вашем коде не создано ни одного Map, умение работать с этой структурой данных все равно может пригодиться. Map-подобные объекты используются в браузерных Web API таких как AudioParamMap, RTCStatsReport, EventCounts, KeyboardLayoutMap, MIDIInputMap и MIDIOutputMap. Такие объекты работают по принципу Map и используют те же методы, однако отличаются ограничениями на типы данных, которые могут быть использованы в качестве ключей.
Set
Set это коллекция уникальных значений без индексов и ключей. Для простоты понимания можно представить что Set это Map, которых хранит только значения, а не пары ключ-значение. Даже методы Set “достались” от Map, но т.к. хранятся не пары, а только само значение любого типа данных, методы .values()
и .keys()
у этой структуры данных полностью аналогичны, а метод .get()
отсутствует. Интересно, что при переборе Set с помощью метода .forEach()
или цикла for…of…
функции будут доступны три аргумента: value, valueAgain (буквально “значение еще раз”) и сама коллекция.
const mySet = new Set([1, 1, "2", "2"]); //Set(2) { 1, '2' }
console.log(mySet.values()); //[Set Iterator] { 1, '2' }
console.log(mySet.keys()); //[Set Iterator] { 1, '2' }
mySet.forEach((value, valueAgain, set) => console.log(value, valueAgain, set));
//1 1 Set(2) { 1, '2' }
//2 2 Set(2) { 1, '2' }
Для добавления нового элемента используется метод .add(value)
. Этот метод назван по-другому не только что бы не создавать путаницы с его названием и названием структуры данных, но и потому что работает совсем иначе. Если переданное значение уже есть в коллекции - метод не производит ни каких действий, что делает его максимально быстрым в сравнении с аналогами у других типов данных. Метод .has(value)
так же производительнее, чем схожий .inclused()
у массивов. При этом важно помнить, что ссылочные типы данных, так же как и в Map, следует передавать именно по ссылке.
const mySet = new Set();
mySet.add({ id: 1, name: "John" }).add({ id: 1, name: "John" });
console.log(mySet); //Set(2) { { id: 1, name: 'John' }, { id: 1, name: 'John' } }
const kate = { id: 2, name: "Kate" };
mySet.add(kate).add(kate);
console.log(mySet);
//Set(3) {{ id: 1, name: 'John' },{ id: 1, name: 'John' },{ id: 2, name: 'Kate' }}
Наиболее частое применение Set - создание коллекций уникальных данных из массивов. Так лаконичная строка кода с использованием Set заменит большую конструкцию с проверкой каждого элемента массива на уникальность и выполнит подобную операцию в разы быстрее.
const myArr = [1, 1, 2, 3, 3];
const uniqueArr = [...new Set(myArr)];//[ 1, 2, 3 ]
Заключение:
Map и Set это удобные инструменты для работы с данными в JavaScript. Они делают код более производительным, лаконичным и легким для чтения. Надеюсь данная статья была полезна для понимания столь важной темы.
Карта развития разработчика
Получите полную карту развития разработчика по всем направлениям: frontend, backend, devops, mobile
Комментарии
0