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.
Related articles
You might also enjoy...
Automate Your Release Notes with AI
How to save time every week using GitLab, OpenAI, and Node.js
11 min read
How to Create a Super Minimal MDX Writing Experience
Learn to create a custom MDX experience so you can focus on writing without worrying about boilerplate or repetition
12 min read
I Fixed Error Handling in JavaScript
How to steal better strategies from Rust and Go—and enforce them with ESLint
14 min read