Коли використовувати Map замість простого об'єкта JavaScript

6 хв. читання

Об'єкти { key: 'value' } в JavaScript призначені для зберігання структурованих даних. Але дещо в них дратує: ключем об'єкта мають бути рядки (або symbols, котрі застосовуються доволі рідко).

Що буде, якщо використати числа як ключі? В такому випадку ми не побачимо жодної помилки:

const names = {
  1: 'One',
  2: 'Two',
};

Object.keys(names); // => ['1', '2']

Просто JavaScript неявно перетворює ключі об'єкта на рядки, але через це втрачається узгодженість типів.

У матеріалі ми з'ясуємо, як доступний з ES2015 об'єкт JavaScript Map розв'язує більшість проблем звичайних об'єктів.

1. У Map можна мати ключ будь-якого типу даних

Як показано вище, якщо ключ об'єкта — не рядок чи symbol, то JavaScript неявно перетворить його тип на рядок.

В об'єкті Map такої проблеми немає:

const numbersMap = new Map();

numbersMap.set(1, 'one');
numbersMap.set(2, 'two');

[...numbersMap.keys()]; // => [1, 2]

numbersMap має ключі 1 та 2 типу даних number. Це прийнятно, тому тип даних не змінюється.

Ви можете використовувати будь-який тип даних для ключа в Map: number, boolean, string та symbol.

const booleansMap = new Map();

booleansMap.set(true, "Yep");
booleansMap.set(false, "Nope");

[...booleansMap.keys()]; // => [true, false]

booleansMap використовує ключі типу boolean без проблем. А от у звичайних об'єктах так не вийде.

Підемо ще далі: чи можна використовувати цілий об'єкт як ключ в Map? Виявляється, можна!

1. 1 Object як ключ

Припустимо, вам треба зберегти деякі дані, що стосуються об'єкта, не створюючи додаткових властивостей об'єкта.

Зі звичайними об'єктами такий трюк не вдасться.

Натомість можна використати масив кортежів об'єкт/значення:

const foo = { name: 'foo' };
const bar = { name: 'bar' };

const kindOfMap = [
  [foo, 'Foo related data'],
  [bar, 'Bar related data'],
];

kindOfMap — масив, який зберігає пари об'єктів та значень, що їх стосуються.

Недолік такого підходу: O(n) складність при отриманні значення за ключем. Тобто необхідно обійти весь масив, аби отримати потрібне значення за ключем:

function getByKey(kindOfMap, key) {
  for (const [k, v] of kindOfMap) {
    if (key === k) {
      return v;
    }
  }
  return undefined;
}

getByKey(kindOfMap, foo); // => 'Foo related data'

Все це набагато полегшується з WeakMap (спеціальної версії Map), яка приймає об'єкти як ключі.

Основна відмінність: WeakMap (на відміну від Map) дозволяє збирання сміття, чим захищає від витоків пам'яті.

Якщо відрефакторимо код вище, вийде щось подібне:

const foo = { name: 'foo' };
const bar = { name: 'bar' };

const mapOfObjects = new WeakMap();

mapOfObjects.set(foo, 'Foo related data');
mapOfObjects.set(bar, 'Bar related data');

mapOfObjects.get(foo); // => 'Foo related data'

WeakMap, порівнюючи з Map, приймає як ключі лише об'єкти і має обмежену кількість методів.

2. У Map немає обмежень щодо назв ключів

Будь-який об'єкт у JavaScript отримує властивості від свого прототипу. Те саме діє і для простих об'єктів.

Якщо ви перевизначаєте властивість, успадковану від прототипу, то потенційно ламаєте код, який використовує цю властивість:

function isPlainObject(value) {
 return value.toString() === '[object Object]';
}

const actor = {
 name: 'Harrison Ford',
 toString: 'Actor: Harrison Ford'
};

// Не спрацює!
isPlainObject(actor); // TypeError: value.toString is not a function

Властивість toString в об'єкті actor перевизначає метод toString(), отриманий від прототипу. Як наслідок, метод, що використовував toString(), тепер не працює.

Подивитись список властивостей та методів, які прості об'єкти успадковують від прототипу, можна за посиланням. Уникайте оголошення користувацьких властивостей з такими назвами.

Припустимо, у нас є форма, яка керує певними користувацькими полями. Користувач може додати поле, визначивши його назву та значення.

Коли використовувати Map замість простого об'єкта JavaScript

Було б зручно зберігати стан користувацьких полів у простому об'єкті:

const userCustomFields = {
  'color':    'blue',
  'size':     'medium',
  'toString': 'A blue box'
};

Однак користувач може вибрати назву на зразок toString, constructor тощо. А це, як ми знаємо, потенційно впливає на інший код.

Не визначайте ключі об'єкта, базуючись на тому, що ввів користувач!

Map допомагає розв'язати цю проблему. Тепер немає обмежень щодо назв ключів:

function isMap(value) {
  return value.toString() === '[object Map]';
}

const actorMap = new Map();

actorMap.set('name', 'Harrison Ford');
actorMap.set('toString', 'Actor: Harrison Ford');

// Працює!
isMap(actorMap); // => true

Хоч в actorMap є ключ toString, цей метод працює коректно.

3. Map можна ітерувати

Щоб ітеруватися властивостями простого об'єкта, треба використовувати додаткові статичні функції на кшталт Object.keys() або Object.entries() (вони доступні в ES2017):

const colorsHex = {
  'white': '#FFFFFF',
  'black': '#000000'
};

for (const [color, hex] of Object.entries(colorsHex)) {
  console.log(color, hex);
}
// 'white' '#FFFFFF'
// 'black' '#000000'

Метод Object.entries(colorsHex) повертає масив пар ключ-значення, отриманих з об'єкта.

Однак Map ми можемо ітерувати одразу:

const colorsHexMap = new Map();

colorsHexMap.set('white', '#FFFFFF');
colorsHexMap.set('black', '#000000');

for (const [color, hex] of colorsHexMap) {
  console.log(color, hex);
}
// 'white' '#FFFFFF'
// 'black' '#000000'

Як бачимо, colorsHexMap ітерується, тому ми можемо використовувати цикли for() та оператор розширення [...map].

Map також пропонує додаткові методи, які повертають ітеровані об'єкти: map.keys() для ітерації ключами та map.values() для ітерації значеннями.

4. Розмір Map

Ще одна незручність при роботі з простими об'єктами — визначення кількості їх властивостей.

const exams = {
  'John Smith': '10 points',
  'Jane Doe': '8 points',
};

Object.keys(exams).length; // => 2

Щоб визначити кількість властивостей в exams, вам необхідно було б пройтися всіма ключами.

Як альтернативу Map пропонує властивість size, котра повертає кількість пар ключ/значення.

const examsMap = new Map([
  ['John Smith', '10 points'],
  ['Jane Doe', '8 points'],
]);
  
examsMap.size; // => 2

Висновок

Простий об'єкт JavaScript чудово вміє зберігати структуровані дані. Однак є декілька обмежень:

  1. Лише рядки чи symbols можуть використовуватись як ключі.
  2. Можуть виникати конфлікти між визначеними властивостями об'єктів та властивостями, успадкованими від прототипу (наприклад, toString, constructor тощо).
  3. Об'єкти не можна використовувати як ключі.

Усі ці проблеми розв'язує Map. Навіть більше, Map значно легше ітерувати та визначати кількість пар ключ/значення. А втім, не слід розглядати Map як заміну простим об'єктам, краще сприймати це як зручне доповнення.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація