How JavaScript Maps Can Make Your Code Faster

Why — and when — to ditch regular JavaScript objects

Published on
Aug 5, 2019

Read time
4 min read

Introduction

Among the goodies introduced to JavaScript in ES6, we saw the introduction of Sets and Maps. Unlike ordinary objects and arrays, these are ‘keyed collections’. That means their behaviour is subtly different and — used in the right contexts — they offer considerable performance advantages.

In a previous article, I looked into Sets and how they can help us write faster, cleaner JavaScript. In this article, I want to do the same for Maps. I’ll discuss how they are different, where they come in handy, and where they can offer performance benefits over regular JavaScript objects.

How are Maps different to Objects?

There are two main differences between Maps and regular JavaScript objects.

1. Unrestricted Keys

Each key in an ordinary JavaScript object must be either a String or a Symbol. The object below demonstrates this:

const symbol = Symbol();
const string2 = "string2";
const regularObject = {
  string1: "value1",
  [string2]: "value2",
  [symbol]: "value3",
};

By contrast, Maps allow you to use functions, objects and any other primitive types (including NaN ) as object keys — as demonstrated below:

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 greater flexibility when it comes to linking different data types.

2. Direct Iteration

To iterate over the keys, values or entries in an object, you must either convert them to an array, using a method like Object.keys(), Object.values() or Object.entries() or use a for ... in loop. Because objects are not directly iterable, the for ... in loop has a couple of restrictions: it only iterates over enumerable, non-Symbol properties and it does so in an arbitrary order.

But Maps are directly iterable, and — because they are a keyed collection — the order of iteration is the same as the order of insertion. To iterate over a Map’s entries, you can use a for ... of loop or the forEach method. The following code shows both:

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

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

A related benefit is that you can call map.size to get the number of entries. To get the equivalent value for an object, you must first convert it to an array like so: Object.keys({}).length.

How are Maps different to Sets?

A Map behaves in a very similar way to a Set, and they share several of the same methods, including has, get, delete and size. Both are keyed collections, meaning that you can use methods like forEach to iterate over the elements in the order of insertion.

The main difference is that a Map is in two dimensions, with elements that come in a key/value pair. Just as you can convert an array to a Set, you can convert a 2D array to 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 a map back to an array, you can use ES6 destructuring assignment syntax:

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

const arr = [...map];

Until recently, it was not so convenient to convert a Map to an object (and vice-versa), so you’d need to rely on a function likes the ones below:

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, with the rollout of ES2019 in August, we have seen the introduction of two new object methods — Object.entries() and Object.fromEntries() — which make this much simpler:

Object.fromEntries(map); // convert Map to object
new Map(Object.entries(obj)); // convert object to Map

Before you use Object.fromEntries to convert a map to an object, ensure that the map’s keys produce unique results when converted to a string. Otherwise, you risk data loss during the conversion.

Performance Tests

To prepare for the tests, I’ll create an object and a Map — each with one million identical keys and values:

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

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

I have used console.time() to benchmark the tests, so that they’re easily repeatable. Though the exact timings can fluctuate — and the ones recorded below will be specific to my system and version of Node.js — my results consistently show performance gains when using Maps, especially when adding and deleting entries.

Finding Entries

let result;

console.time("Object");

result = obj.hasOwnProperty("999999");

console.timeEnd("Object");
console.time("Map");

result = map.has(999999);

console.timeEnd("Map");

Object: 0.250ms
Map: 0.095ms (2.6 times faster)

Adding Entries

console.time("Object");

obj[n] = n;

console.timeEnd("Object");
console.time("Map");

map.set(n, n);

console.timeEnd("Map");

Object: 0.229ms
Map: 0.005ms (45.8 times faster!)

Deleting Entries

console.time("Object");

delete obj[n];

console.timeEnd("Object");
console.time("Map");

map.delete(n);

console.timeEnd("Map");

Object: 0.376ms
Map: 0.012ms(31 times faster!)

Where Maps Are Slower

In my tests, I found one case where objects outperformed Maps: when using a for loop to create our original object and map. This result is surprising, since without the for loop, adding entries to a Map outperformed adding entries to a standard object.

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

Object: 32.143ms
Map: 163.828ms (5 times slower)

An Example Use Case

Finally, let’s look at a case where a Map would be preferable to an object. Say we had to write a function to determine whether two strings where anagrams of one another:

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

There are multiple ways to do this, but here, maps can help us create one of the fastest solutions:

Here, maps are preferable to objects when we need to add and remove values dynamically and because we don’t know the shape of the data (or the number of entries) in advance.

I hope you found this article useful, and that — if you hadn’t encountered Maps before — it opened your eyes to a valuable part of modern JavaScript.

© 2024 Bret Cameron