How to Use JavaScript Map to Improve Performance

In introducing the new features of JavaScript in ES6, we saw the introduction of Set and Map. Unlike regular objects and Array, they are "keyed collections". This means that they behave slightly differently and are used in specific contexts, providing considerable performance advantages.

In this article, I will analyze Map, how it differs, where it can be used, and what performance advantages it has over conventional objects.

What's the difference between a Map and a regular object?

Map and regular objects have two main differences.

1. Unlimited Key

The key of a regular JavaScript object must be String or Symbol. The following object illustrates this point:

const symbol = Symbol();
const string2 = 'string2';

const regularObject = {
  string1: 'value1',
  [string2]: 'value2',
  [symbol]: 'value3'
};

Map, by contrast, allows you to use functions, objects, and other simple types (including NaN) as keys, as follows:

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');

This feature provides great flexibility in linking different data types.

2. Direct traversal

In a regular object, in order to traverse keys, values, and entries, you must convert them into arrays, such as using Object.keys(), Object.values(), and Object.entries(), or using a for... in loop, because a regular object cannot traverse directly, and there are some limitations to the for... in loop: it only traverses enumerable belongings. Sex, non-Symbol attributes, and the order of traversal is arbitrary.

Map can traverse directly, and because it is a set of keys, the order of traversal is the same as that of inserting keys. You can use the for... of loop or forEach method to traverse Map entries, as follows:

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

Another advantage is that you can call the map.size property to get the number of keys, while for regular objects, you have to convert to an array first, and then get the length of the array, such as: Object.keys({}).length.

What's the difference between Map and Set?

Map s behave very similar to Sets, and they all contain the same methods: has, get, set, delete. Both of them are keyed sets, which means that you can traverse elements using methods like forEach, in order of insertion keys.

The biggest difference is that Maps appear in pairs through key/value, just as you can convert an array into a Set, or you can convert a two-dimensional array into a Map:

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

Type Conversion

To switch Map back to an array, you can use ES6's structural syntax:

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

So far, it's still not very convenient to convert Map to regular objects, so you may need to rely on a functional approach, as follows:

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;
};

But now, in the first presentation of ES2019 in August, we saw Object introduce two new methods: Object.entries() and Object.fromEntries(), which can simplify the above methods a lot:

const obj2 = Object.fromEntries(map);
const map2 = new Map(Object.entries(obj));

Before you convert a map to an object using Object.fromEntries, make sure that the key of the map produces a unique result when converted to a string, or you risk losing data.

performance testing

To prepare for the test, I'll create an object and a map with 1000,000 identical keys.

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

Then I use console.time() to measure the test, because of my particular system and the Node.js version, the time accuracy may fluctuate. The test results show the performance benefits of using Map, especially when adding and deleting key values.

query

let result;
console.time('Object');
result = obj.hasOwnProperty('999999');
console.timeEnd('Object');
// Object: 0.250ms

console.time('Map');
result = map.has(999999);
console.timeEnd('Map');
// Map: 0.095ms (2.6 times faster)

Add to

console.time('Object');
obj[n] = n;
console.timeEnd('Object');
// Object: 0.229ms

console.time('Map');
map.set(n, n);
console.timeEnd('Map');
// Map: 0.005ms (45.8 times faster!)

delete

console.time('Object');
delete obj[n];
console.timeEnd('Object');
// Object: 0.376ms

console.time('Map');
map.delete(n);
console.timeEnd('Map');
// Map: 0.012ms (31 times faster!)

When is Map Slower

In testing, I found that in one case, regular objects perform better: using for loops to create regular objects and maps. This result is shocking, but without a for loop, map adds attributes better than regular objects.

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

let obj = {}, map = new Map(), n = 1000000;
console.time('Map');
for (let i = 0; i < n; i++) {
  map.set(i, i);
}
console.timeEnd('Map');
// Map: 163.828ms (5 times slower)

For instance

Finally, let's look at an example where a Map is more appropriate than a regular object. For example, we want to write a function to check whether two strings are randomly sorted by the same string.

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

There are many ways to do this, but here, map can help us create the simplest and fastest solution:

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;
};

In this example, map is more appropriate than object when it involves dynamically adding and deleting key values and failing to confirm the data structure (or the number of key values) in advance.

I hope this article will help you. If you haven't used Map before, you might as well broaden your horizon and measure the value of modern JavaScript.

Translator's Note: Personally, I don't agree with the author's point of view. From the above description, Map is more like a trade-off between space and speed. So there must be a threshold for measuring space and speed. When the amount of data is small, the cost of space is greater than that of speed. It is obviously not suitable to use Map at this time. When the amount of data is large enough, the cost of space has less impact. So, how do developers measure the relationship between the two and choose the best solution?

Links to the original text

Keywords: Javascript less

Added by fooDigi on Tue, 13 Aug 2019 10:15:22 +0300