JavaScript Maps vs Обычные объекты: смертельная битва

Стандарт ES6 дал в руки разработчикам мощное и гибкое оружие – JavaScript Maps. Кажется, простым объектам уже пора на покой... Или нет?


JavaScript Maps vs Обычные объекты: смертельная битва

Среди других замечательных плюшек, в стандарте ES6 появились мапы (Maps) и сеты (Sets). У этих коллекций большие возможности и удобные методы. При использовании в правильных местах они могут дать выигрыш в производительности. Пора ли отказываться от старых-добрых объектов и массивов, или новые структуры не могут полностью их заменить?

Эта статья посвящена мапам – их специфике, преимуществам и тонкостям использования. Вы также можете узнать больше о том, как JavaScript Sets ускоряют код.

JavaScript Maps vs Objects

Между мапами и простыми объектами есть два принципиальных отличия.

1. Нет ограничений на ключи

Обычные объекты ставят нам четкое условие: ключ должен быть строкой (String) или символом (Symbol).

const symbol = Symbol();
const string2 = String();
const regularObject = {
   string1: 'value1',
   string2: 'value2',
   symbol: 'value3'
};

Если вам очень хочется использовать в качестве ключа другой объект или, может быть, функцию – увы, дорогой JavaScript разработчик…

Хотя почему бы и нет, ведь теперь у нас есть мапы! Захотелось вам сделать ключом массив – будьте любезны:

const func = () => null;
const object = {};
const array = [];
const bool = false;
const map = new Map();

map.set(func, 'value1');
map.set(object, 'value2');
map.set(array, 'value3');
map.set(bool, 'value4');
map.set(NaN, 'value5');

Объекты, массивы, функции, примитивные типы, даже NaN – никаких ограничений больше нет. Это очень-очень гибко, ведь теперь вы можете связать друг с другом любые данные.

Прямая итерация

Нельзя просто так взять и перебрать объект.

JavaScript Maps vs Обычные объекты: смертельная битва

Сначала нужно преобразовать его в некоторое подобие массива, используя Object.keys(), Object.values() или Object.entries().

Есть, конечно, цикл for … in, но с ним всегда довольно много проблем:

  • он перебирает только перечисляемые (enumerable) свойства;
  • не работает с полями-символами;
  • не сохраняет исходный порядок свойств в коллекции.

JavaScript Maps в отличие от объектов прекрасно перебираются напрямую, причем порядок итерации полей всегда соответствует порядку их вставки в мап.

for (let [key, value] of map) {
  console.log(key);
  console.log(value);
};

map.forEach((key, value) => {
  console.log(key);
  console.log(value);
});

Благодаря этому можно легко получить размер коллекции – map.size. А чтобы узнать, сколько свойств хранится в объекте, приходится вызывать сложную конструкцию Object.keys({}).length.

JavaScript Maps vs Sets

Мапы очень похожи на сеты по интерфейсу – у них целый набор одинаковых методов и свойств: has, get, delete, size. Обе структуры можно проитерировать напрямую с сохранением порядка в цикле или перебирающими методами вроде forEach.

Основное различие заключается в количестве измерений. В Set оно одно, как в простом массиве, а в Map – два:

const set = new Set([1, 2, 3, 4]);

const map = new Map([['one', 1], ['two', 2], ['three', 3], ['four', 4]]);

Преобразование типов

Если уж мы превращаем массивы в JavaScript Maps, может потребоваться и обратное преобразование. Используйте для этого синтаксис деструктурирующего присваивания, введенный новым стандартом:

const map = new Map([['one', 1], ['two', 2]]);

const arr = [...map];

Сложнее обстоит дело с конвертацией мапы в объект (и обратно). Для этого придется написать специальную функцию:

const mapToObj = map => {
  const obj = {};
  map.forEach((key, value) => { obj[key] = value });
  return obj;
};

const objToMap = obj => {
  const map = new Map;
  Object.keys(obj).forEach(key => { map.set(key, obj[key]) });
  return map;
};

Впрочем, язык JavaScript заботится о своих адептах – в новом стандарте ES2019 появились два метода, которые смогут решить эту задачу в одну строку – Object.entries() и Object.fromEntries():

Object.fromEntries(map); // Map -> object

new Map(Object.entries(obj)); // object -> Map

Убедитесь, что ключи вашей мапы останутся уникальными при преобразовании к строке, прежде чем конвертировать ее в объект, иначе вы потеряете часть данных.

Тесты производительности

А теперь самое интересное. Испытаем JavaScript Maps в деле. Вдруг они окажутся ужасно медленными и неповоротливыми…

Создадим обычный объект и мапу – каждый с миллионом свойств.

let obj = {}, map = new Map(), n = 1000000;
for (let i = 0; i < n; i++) {
  obj[i] = i;
  map.set(i, i);
}

Для бенчмаркинга будем использовать обычный метод console.time. Точные цифры, разумеется, будут зависеть от системы и среды, в которой запускается код, но нам интереснее общая картина – рост или падение производительности.

Спойлер от автора: наблюдается стабильное ускорение работы программы при использовании мап, особенно при добавлении и удалении свойств.

Поиск значений

let result;

console.time('Object');
result = obj.hasOwnProperty('999999');
console.timeEnd('Object');

console.time('Map');
result = map.has(999999);
console.timeEnd('Map');

Объект: 0.250 мс
Мапа: 0.095 мс (в 2.6 раз быстрее)

Добавление значений

console.time('Object');
obj[n] = n;
console.timeEnd('Object');

console.time('Map');
map.set(n, n);
console.timeEnd('Map');

Объект: 0.229мс
Мапа: 0.005мс (в 45.8 раз быстрее)

Удаление значений

console.time('Object');
delete obj[n];
console.timeEnd('Object');

console.time('Map');
map.delete(n);
console.timeEnd('Map');

Объект: 0.376мс
Мапа: 0.012мс (в 31 раз быстрее)

Примечание от переводчика

При прогоне тестов в консоли браузера с теми же самыми исходными данными результаты получились не столь ошеломляющие.

Для проверки наличия значения особой разницы не замечено:

JavaScript Maps vs Обычные объекты: смертельная битва

При добавлении и удалении значений Map даже проигрывает:

JavaScript Maps vs Обычные объекты: смертельная битва
Тест производительности: добавление значений в JavaScript Maps
JavaScript Maps vs Обычные объекты: смертельная битва
Тест производительности: удаление значений из JavaScript Maps

Впрочем, автор отмечает, что внутри цикла for объекты работают быстрее, чем JavaScript Maps – довольно интригующее наблюдение:

let obj = {}, map = new Map(), n = 1000000;

console.time('Map');
for (let i = 0; i < n; i++) {
  map.set(i, i);
}
console.timeEnd('Map');

console.time('Object');
for (let i = 0; i < n; i++) {
  obj[i] = i;
}
console.timeEnd('Object');

Объект: 32.143мс
Мапа: 163.828мс (в 5 раз медленнее)

Использование JavaScript Maps

Итак, в каких случаях JavaScript разработчик должен использовать мапы, а не обычные объекты?

Предположим, вам нужно написать функцию isAnagram, которая будет определять, являются ли два слова анаграммами друг друга:

console.log(isAnagram('anagram', 'gramana')); // true
console.log(isAnagram('anagram', 'margnna')); // false

Способов решить эту задачку много, однако мапы предлагают одно из самых чистых и быстрых решений:

const isAnagram = (str1, str2) => {
  if (str1.length !== str2.length) return false;
  const map = new Map();
  for (let char of str1) {
    const count = map.has(char) ? map.get(char) + 1 : 1;
    map.set(char, count);
  }
  for (let char of str2) {
    if (!map.has(char)) return false;
    const count = map.get(char) - 1;
    if (count === 0) {
      map.delete(char);
      continue;
    }
    map.set(char, count);
  }
  return map.size === 0;
}

Если необходимо добавлять и удалять значения динамически, использовать JavaScript Maps предпочтительнее, чем объекты. Кроме того, они очень полезны, если типы данных или количество записей заранее не известны.

С мапами программирование на JavaScript стало еще немного проще, но, конечно, это не панацея от всех бед.

Как вы используете мапы в своих проектах?

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Go-разработчик
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ