HomepageExploring JavaScript (ES2024 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

35 Maps (Map) [ES6]

Before ES6, JavaScript didn’t have a data structure for dictionaries and (ab)used objects as dictionaries from strings to arbitrary values. ES6 brought Maps, which are dictionaries from arbitrary values to arbitrary values.

35.1 Using Maps

An instance of Map maps keys to values. A single key-value mapping is called an entry.

35.1.1 Creating Maps

There are three common ways of creating Maps.

First, we can use the constructor without any parameters to create an empty Map:

const emptyMap = new Map();
assert.equal(emptyMap.size, 0);

Second, we can pass an iterable (e.g., an Array) over key-value “pairs” (Arrays with two elements) to the constructor:

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'], // trailing comma is ignored
]);

Third, the .set() method adds entries to a Map and is chainable:

const map = new Map()
  .set(1, 'one')
  .set(2, 'two')
  .set(3, 'three');

35.1.2 Copying Maps

As we’ll see later, Maps are also iterables over key-value pairs. Therefore, we can use the constructor to create a copy of a Map. That copy is shallow: keys and values are the same; they are not duplicated.

const original = new Map()
  .set(false, 'no')
  .set(true, 'yes');

const copy = new Map(original);
assert.deepEqual(original, copy);

35.1.3 Working with single entries

.set() and .get() are for writing and reading values (given keys).

const map = new Map();

map.set('foo', 123);

assert.equal(map.get('foo'), 123);
// Unknown key:
assert.equal(map.get('bar'), undefined);
// Use the default value '' if an entry is missing:
assert.equal(map.get('bar') ?? '', '');

.has() checks if a Map has an entry with a given key. .delete() removes entries.

const map = new Map([['foo', 123]]);

assert.equal(map.has('foo'), true);
assert.equal(map.delete('foo'), true)
assert.equal(map.has('foo'), false)

35.1.4 Determining the size of a Map and clearing it

.size contains the number of entries in a Map. .clear() removes all entries of a Map.

const map = new Map()
  .set('foo', true)
  .set('bar', false)
;

assert.equal(map.size, 2)
map.clear();
assert.equal(map.size, 0)

35.1.5 Getting the keys and values of a Map

.keys() returns an iterable over the keys of a Map:

const map = new Map()
  .set(false, 'no')
  .set(true, 'yes')
;

for (const key of map.keys()) {
  console.log(key);
}

Output:

false
true

We use Array.from() to convert the iterable returned by .keys() to an Array:

assert.deepEqual(
  Array.from(map.keys()),
  [false, true]);

.values() works like .keys(), but for values instead of keys.

35.1.6 Getting the entries of a Map

.entries() returns an iterable over the entries of a Map:

const map = new Map()
  .set(false, 'no')
  .set(true, 'yes')
;

for (const entry of map.entries()) {
  console.log(entry);
}

Output:

[ false, 'no' ]
[ true, 'yes' ]

Array.from() converts the iterable returned by .entries() to an Array:

assert.deepEqual(
  Array.from(map.entries()),
  [[false, 'no'], [true, 'yes']]);

Map instances are also iterables over entries. In the following code, we use destructuring to access the keys and values of map:

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

Output:

false no
true yes

35.1.7 Listed in insertion order: entries, keys, values

Maps record in which order entries were created and honor that order when listing entries, keys, or values:

const map1 = new Map([
  ['a', 1],
  ['b', 2],
]);
assert.deepEqual(
  Array.from(map1.keys()), ['a', 'b']);

const map2 = new Map([
  ['b', 2],
  ['a', 1],
]);
assert.deepEqual(
  Array.from(map2.keys()), ['b', 'a']);

35.1.8 Converting between Maps and Objects

As long as a Map only uses strings and symbols as keys, we can convert it to an object (via Object.fromEntries()):

const map = new Map([
  ['a', 1],
  ['b', 2],
]);
const obj = Object.fromEntries(map);
assert.deepEqual(
  obj, {a: 1, b: 2});

We can also convert an object to a Map with string or symbol keys (via Object.entries()):

const obj = {
  a: 1,
  b: 2,
};
const map = new Map(Object.entries(obj));
assert.deepEqual(
  map, new Map([['a', 1], ['b', 2]]));

35.2 Example: Counting characters

countChars() returns a Map that maps characters to numbers of occurrences.

function countChars(chars) {
  const charCounts = new Map();
  for (let ch of chars) {
    ch = ch.toLowerCase();
    const prevCount = charCounts.get(ch) ?? 0;
    charCounts.set(ch, prevCount+1);
  }
  return charCounts;
}

const result = countChars('AaBccc');
assert.deepEqual(
  Array.from(result),
  [
    ['a', 2],
    ['b', 1],
    ['c', 3],
  ]
);

35.3 A few more details about the keys of Maps (advanced)

Any value can be a key, even an object:

const map = new Map();

const KEY1 = {};
const KEY2 = {};

map.set(KEY1, 'hello');
map.set(KEY2, 'world');

assert.equal(map.get(KEY1), 'hello');
assert.equal(map.get(KEY2), 'world');

35.3.1 What keys are considered equal?

Most Map operations need to check whether a value is equal to one of the keys. They do so via the internal operation SameValueZero, which works like === but considers NaN to be equal to itself.

As a consequence, we can use NaN as a key in Maps, just like any other value:

> const map = new Map();

> map.set(NaN, 123);
> map.get(NaN)
123

Different objects are always considered to be different. That is something that can’t be changed (yet – configuring key equality is on TC39’s long-term roadmap).

> new Map().set({}, 1).set({}, 2).size
2

35.4 Missing Map operations

35.4.1 Mapping and filtering Maps

We can .map() and .filter() an Array, but there are no such operations for a Map. The solution is:

  1. Convert the Map to an Array of [key, value] pairs.
  2. Map or filter the Array.
  3. Convert the result back to a Map.

I’ll use the following Map to demonstrate how that works.

const originalMap = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');

Mapping originalMap:

const mappedMap = new Map( // step 3
  Array.from(originalMap) // step 1
  .map(([k, v]) => [k * 2, '_' + v]) // step 2
);
assert.deepEqual(
  Array.from(mappedMap),
  [[2,'_a'], [4,'_b'], [6,'_c']]);

Filtering originalMap:

const filteredMap = new Map( // step 3
  Array.from(originalMap) // step 1
  .filter(([k, v]) => k < 3) // step 2
);
assert.deepEqual(Array.from(filteredMap),
  [[1,'a'], [2,'b']]);

Array.from() converts any iterable to an Array.

35.4.2 Combining Maps

There are no methods for combining Maps, which is why we must use a workaround that is similar to the one from the previous section.

Let’s combine the following two Maps:

const map1 = new Map()
  .set(1, '1a')
  .set(2, '1b')
  .set(3, '1c')
;

const map2 = new Map()
  .set(2, '2b')
  .set(3, '2c')
  .set(4, '2d')
;

To combine map1 and map2 we create a new Array and spread (...) the entries (key-value pairs) of map1 and map2 into it (via iteration). Then we convert the Array back into a Map. All of that is done in line A:

const combinedMap = new Map([...map1, ...map2]); // (A)
assert.deepEqual(
  Array.from(combinedMap), // convert to Array for comparison
  [ [ 1, '1a' ],
    [ 2, '2b' ],
    [ 3, '2c' ],
    [ 4, '2d' ] ]
);

Icon “exercise”Exercise: Combining two Maps

exercises/maps/combine_maps_test.mjs

35.5 Quick reference: Map

Note: For the sake of conciseness, I’m pretending that all keys have the same type K and that all values have the same type V.

35.5.1 new Map()

35.5.2 Map.*

35.5.3 Map.prototype.*: handling single entries

35.5.4 Map.prototype: handling all entries

35.5.5 Map.prototype: iterating and looping

Both iterating and looping happen in the order in which entries were added to a Map.

35.6 FAQ: Maps

35.6.1 When should I use a Map, and when should I use an object?

If we need a dictionary-like data structure with keys that are neither strings nor symbols, we have no choice: we must use a Map.

If, however, our keys are either strings or symbols, we must decide whether or not to use an object. A rough general guideline is:

35.6.2 When would I use an object as a key in a Map?

We normally want Map keys to be compared by value (two keys are considered equal if they have the same content). That excludes objects. However, there is one use case for objects as keys: externally attaching data to objects. But that use case is served better by WeakMaps, where entries don’t prevent keys from being garbage-collected (for details, consult the next chapter).

35.6.3 Why do Maps preserve the insertion order of entries?

In principle, Maps are unordered. The main reason for ordering entries is so that operations that list entries, keys, or values are deterministic. That helps, for example, with testing.

35.6.4 Why do Maps have a .size, while Arrays have a .length?

In JavaScript, indexable sequences (such as Arrays and strings) have a .length, while unindexed collections (such as Maps and Sets) have a .size: