логотип PurpleSchool

Map и Set в JavaScript для удобной работы с данными

14 августа 2023 г.
404 просмотра
фото команды
Автор

Валерий

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

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. Они делают код более производительным, лаконичным и легким для чтения. Надеюсь данная статья была полезна для понимания столь важной темы.

изображение курса

Продвинутый JavaScript

Антон Ларичев
иконка часов16 часов лекций
иконка зведочки рейтинга4.9
Продвинутый JavaScript