Зараз ми знаємо про наступні складні структури даних:
- Об’єкти для зберігання іменованих колекцій.
- Масиви для зберігання впорядкованих колекцій.
Але цього не завжди достатньо в реальному житті. Ось чому існують Map
та Set
.
Map
Map – це колекція ключ/значення, як і Object
. Але основна відмінність полягає в тому, що Map
дозволяє мати ключі будь-якого типу.
Методи та властивості:
new Map()
– створює колекцію.map.set(key, value)
– зберігає значенняvalue
за ключемkey
.map.get(key)
– повертає значення за ключем; повертаєundefined
якщоkey
немає в колекції.map.has(key)
– повертаєtrue
якщоkey
існує, інакшеfalse
.map.delete(key)
– видаляє елемент (пару ключ/значення) за ключем.map.clear()
– видаляє всі елементи колекції.map.size
– повертає поточну кількість елементів.
Наприклад:
let map = new Map();
map.set('1', 'str1'); // рядок як ключ
map.set(1, 'num1'); // цифра як ключ
map.set(true, 'bool1'); // булеве значення як ключ
// пам’ятаєте звичайний об’єкт `Object`? Він перетворює всі ключі в рядок
// Map зберігає тип ключів, так що в цьому випадку ми отримаємо 2 різних значення:
alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1'
alert( map.size ); // 3
Як ми бачимо, на відміну від об’єктів, ключі не були приведені до рядків. Можна використовувати будь-які типи даних для ключів.
map[key]
не є правильним методом використання Map
Хоча map[key]
також працює, але такий спосіб присвоєння map[key] = 2
використовує колекцію як звичайний JavaScript об’єкт, тобто накладає відповідні обмеження (тільки типи рядки/символи як ключі та інше).
Таким чином ми повинні користуватись map
методами: set
, get
і так далі.
Map також може використовувати об’єкти як ключі.
Наприклад:
let ivan = { name: "Іван" };
// збережімо кількість відвідувань для кожного користувача
let visitsCountMap = new Map();
// об’єкт ivan -- це ключ для значення в колекції Map
visitsCountMap.set(ivan, 123);
alert( visitsCountMap.get(ivan) ); // 123
Об’єкти в якості ключів – це одна з відомих можливостей колекції Map
, яку часто використовують. У звичайному об’єкті Object
, ми можемо використати ключі-рядки, проте ключі-об’єкти – вже ні.
Розгляньмо такий приклад:
let ivan = { name: "Іван" };
let bohdan = { name: "Богдан" };
let visitsCountObj = {}; // оголосимо звичайний об’єкт
visitsCountObj[bohdan] = 234; // використаємо об’єкт `bohdan` як ключ
visitsCountObj[ivan] = 123; // використаємо `ivan` об’єкт як ключ, `bohdan` об’єкт буде перезаписаний
// Ось як це було записано!
alert( visitsCountObj["[object Object]"] ); // 123
Оскільки visitsCountObj
– це об’єкт, він конвертує всі ключі типу Object
(такі як ivan
і bohdan
) до рядка "[object Object]"
. Це однозначно не той результат, який ми очікуємо.
Map
порівнює ключіПорівнюючи ключі, об’єкт Map
використовує алгоритм SameValueZero. Це майже таке ж порівняння, що і ===
, з тією лише різницею, що NaN
вважається рівним NaN
. Таким чином NaN
може також бути використаний як ключ.
Цей алгоритм не може бути замінений або модифікований.
Кожен виклик map.set
повертає об’єкт map, таким чином ми можемо об’єднати виклики в ланцюжок:
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
Перебір Map
Для перебору колекції Map є 3 метода:
map.keys()
– повертає об’єкт-ітератор для ключів,map.values()
– повертає об’єкт-ітератор для значень,map.entries()
– повертає об’єкт-ітератор зі значеннями виду [ключ, значення], цей варіант типово використовується зfor..of
.
Наприклад:
let recipeMap = new Map([
['огірок', 500],
['помідори', 350],
['цибуля', 50]
]);
// перебираємо ключі (овочі)
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // огірок, помідори, цибуля
}
// перебираємо значення (кількість)
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// перебір елементів у форматі [ключ, значення]
for (let entry of recipeMap) { // те ж саме, що recipeMap.entries()
alert(entry); // огірок,500 (і так далі)
}
На відміну від звичайних об’єктів Object
, в Map
перебір відбувається в тому ж порядку, в якому відбувалося додавання елементів.
Крім цього, Map
має вбудований метод forEach
, схожий з вбудованим методом масивів Array
:
// виконуємо функцію для кожної пари (ключ, значення)
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // огірок: 500 і так далі
});
Object.entries: Map з Object
При створенні Map
ми можемо вказати масив (або інший об’єкт-ітератор) з парами ключ-значення для ініціалізації, як тут:
// масив пар [ключ, значення]
let map = new Map([
['1', 'str1'],
[1, 'num1'],
[true, 'bool1']
]);
alert( map.get('1') ); // str1
Якщо у нас вже є звичайний об’єкт, і ми б хотіли створити з нього Map
, то допоможе вбудований метод Object.entries(obj), котрий отримує об’єкт і повертає масив пар ключ-значення для нього, як раз в цьому форматі.
Таким чином ми можемо створити Map
з об’єкта наступним чином:
let obj = {
name: "Іван",
age: 30
};
let map = new Map(Object.entries(obj));
alert( map.get('name') ); // Іван
В цьому випадку Object.entries
повертає масив пар ключ-значення: [ ["name", "Іван"], ["age", 30] ]
. Це саме те, що потрібно для створення Map
.
Object.fromEntries: Object з Map
Ми щойно створювали Map
з простого об’єкта за допомогою Object.entries(obj)
.
Ми можемо використати Object.fromEntries
метод, який виконає зворотну дію: трансформує отриманий масив пар [ключ, значення]
в об’єкт. Наприклад:
let prices = Object.fromEntries([
['банан', 1],
['апельсин', 2],
['яблуко', 4]
]);
// тепер prices = { банан: 1, апельсин: 2, яблуко: 4 }
alert(prices.апельсин); // 2
Ми можемо використати Object.fromEntries
, щоб отримати звичайний об’єкт з Map
.
Наприклад, ми маємо дані в Map
, але потрібно їх передати в сторонній код, який чекає простий об’єкт.
Ось як це зробити:
let map = new Map();
map.set('банан', 1);
map.set('апельсин', 2);
map.set('яблуко', 4);
let obj = Object.fromEntries(map.entries()); // робимо простий об’єкт (*)
// Готово!
// obj = { банан: 1, апельсин: 2, яблуко: 4 }
alert(obj.апельсин); // 2
Виклик map.entries()
повертає масив пар ключ/значення, як раз в потрібному форматі для Object.fromEntries
.
Ми могли б написати рядок (*)
ще коротше:
let obj = Object.fromEntries(map); // прибрати .entries()
Це те ж саме, оскільки Object.fromEntries
чекає аргументом об’єкт-ітератор, не обов’язково масив. А перебір map
якраз повертає пари ключ/значення, як і map.entries()
. Так що в підсумку ми матимемо звичайний об’єкт з тими ж ключами/значеннями, що і в map
.
Set
Об’єкт Set
– це особливий тип колекції: “множина” значень (без ключів), де кожне значення може з’являтися тільки раз.
Основні методи:
new Set([iterable])
– створюєSet
, якщо аргументом виступає об’єкт-ітератор, тоді значення копіюються вSet
.set.add(value)
– додає нове значення доSet
, повертаєSet
.set.delete(value)
– видаляє значення зSet
, повертаєtrue
, якщоvalue
наявне в множині значень на момент виклику методу, інакшеfalse
.set.has(value)
– повертаєtrue
, якщоvalue
присутнє в множиніSet
, інакшеfalse
.set.clear()
– видаляє всі значення множиниSet
.set.size
– повертає кількість елементів в множині.
Родзинкою Set
є виклики set.add(value)
, що повторюються з однаковими значеннями value
. Повторні виклики цього методу не змінюють Set
. Це причина того, що кожне значення з’являється тільки один раз.
Наприклад, ми очікуємо гостей, і нам необхідно скласти їх список. Але повторні записи не повинні призводити до дублікатів. Кожен гість повинен з’явитися в списку лише один раз.
Множина Set
– це саме те, що потрібно для цього:
let set = new Set();
let ivan = { name: "Іван" };
let petro = { name: "Петро" };
let maria = { name: "Марія" };
// підраховуємо гостей, деякі приходять кілька разів
set.add(ivan);
set.add(petro);
set.add(maria);
set.add(ivan);
set.add(maria);
// set зберігає тільки 3 унікальних значення
alert( set.size ); // 3
for (let user of set) {
alert(user.name); // "Іван" (тоді "Петро" і "Марія")
}
Альтернативою множини Set
може виступати масив для зберігання гостей і додатковий код для перевірки вже наявного елемента за допомогою arr.find. Але в цьому випадку буде гірша продуктивність, тому що arr.find
проходить весь масив для перевірки наявності елемента. Множина Set
краще оптимізована для перевірки унікальності.
Перебір об’єкта Set
Ми можемо перебрати вміст об’єкта set
як за допомогою методу for..of
, так і використовуючи forEach
:
let set = new Set(["апельсини", "яблука", "банани"]);
for (let value of set) alert(value);
// те ж саме з forEach:
set.forEach((value, valueAgain, set) => {
alert(value);
});
Зауважимо цікаву річ. Функція в forEach
у Set
має 3 аргументи: значення ‘value’, потім знову те саме значення ‘valueAgain’, і тільки потім цільовий об’єкт. Це дійсно так, значення з’являється в списку аргументів двічі.
Це зроблено для сумісності з об’єктом Map
, в якому колбек forEach
має 3 аргумента. Виглядає трохи дивно, але в деяких випадках може допомогти легко замінити Map
на Set
і навпаки.
Set
має ті ж вбудовані методи, що і Map
:
set.keys()
– повертає об’єкт-ітератор для значень,set.values()
– те ж саме, щоset.keys()
, для сумісності зMap
,set.entries()
– повертає об’єкт-ітератор для пар виду[значення, значення]
, присутній для сумісності зMap
.
Підсумки
Map
– це колекція ключ/значення.
Методи та властивості:
new Map([iterable])
– створює колекцію, можна вказатиоб’єкт-ітератор
(зазвичай масив) з пар[ключ, значення]
для ініціалізації.map.set(key, value)
– записує по ключуkey
значенняvalue
.map.get(key)
– повертає значення поkey
абоundefined
, якщо ключkey
відсутній.map.has(key)
– повертаєtrue
, якщо ключkey
присутній в колекції, інакшеfalse
.map.delete(key)
– видаляє елемент по ключуkey
. Повертаєtrue
, якщоkey
існує на момент виклику функції, інакшеfalse
.map.clear()
– очищає колекцію від всіх елементів.map.size
– повертає поточну кількість елементів.
Відмінності від звичайного об’єкта Object
:
- Що завгодно може бути ключем, в тому числі і об’єкти.
- Є додаткові методи, властивість
size
.
Set
– колекція унікальних значень, так звана “множина”.
Методи та властивості:
new Set([iterable])
– створюєSet
, можна вказатиоб’єкт-ітератор
(зазвичай масив).set.add(value)
– додає значення (якщо воно вже є, то нічого не робить), повертає той же об’єктset
.set.delete(value)
– видаляє значення, повертаєtrue
якщоvalue
було в множині на момент виклику, інакшеfalse
.set.has(value)
– повертаєtrue
, якщо значення присутній в множині, інакшеfalse
.set.clear()
– видаляє всі наявні значення.set.size
– повертає кількість елементів у множині.
Перебір Map
і Set
завжди здійснюється в порядку додавання елементів, так що не можна сказати, що це невпорядковані колекції, але поміняти порядок елементів або отримати елемент безпосередньо по його номеру не можна.