JavaScriptMaps

Map

A Map is a keyed collection — like a plain object, but built from the ground up to be a dictionary. Its keys can be anything, not just strings. Its iteration order is the order keys were inserted. And the API distinguishes between "I want a value" and "I want to know if the key exists" without prototype-chain surprises. For anything resembling a lookup table, Map is almost always the right tool.

Creating a Map

JS
const empty = new Map();

const fromEntries = new Map([
  ["alice", 90],
  ["bob",   85],
  ["carol", 75],
]);

console.log(fromEntries.get("bob"));   // 85
85

The constructor takes any iterable of two-element [key, value] arrays. That makes it round-trip nicely with Object.entries, fetch form data, and other Maps.

get, set, has, delete
  • map.set(key, value) — adds or updates. Returns the map (chainable).

  • map.get(key) — returns the value, or undefined if absent.

  • map.has(key) — true/false. Use this when undefined is a valid value.

  • map.delete(key) — removes; returns true if the key was there.

  • map.clear() — empties.

  • map.size — number of entries (a property, not a method).

JS
const cache = new Map();
cache.set("user:1", { name: "Ada" })
     .set("user:2", { name: "Lin" });

console.log(cache.has("user:1"));    // true
console.log(cache.get("user:3"));    // undefined
console.log(cache.size);             // 2
Any key type

Plain objects coerce their keys to strings — obj[1] and obj["1"] are the same property. Map keys keep their type. A function, a number, a DOM node and an object literal are all valid keys.

JS
const m = new Map();
m.set(1, "number one");
m.set("1", "string one");

console.log(m.get(1));     // number one
console.log(m.get("1"));   // string one

const key = {};
m.set(key, "by reference");
console.log(m.get(key));         // by reference
console.log(m.get({}));          // undefined — different object reference

Object keys compare by reference, just like in Set. Two object literals with the same shape are different keys.

Iteration order

Iteration walks entries in insertion order, the same way Set does. Re-setting an existing key updates its value without changing its position.

JS
const m = new Map([["a", 1], ["b", 2], ["c", 3]]);
m.set("b", 99);          // updates b, but b stays in the middle

for (const [k, v] of m) console.log(k, v);
a 1
b 99
c 3
Iterating

A Map is iterable: for...of yields [key, value] pairs. Three helper methods give you different views.

JS
const m = new Map([["a", 1], ["b", 2]]);

for (const [k, v] of m.entries()) console.log(k, v);
for (const k    of m.keys())      console.log(k);
for (const v    of m.values())    console.log(v);

m.forEach((value, key) => console.log(key, "=", value));

forEach's callback is (value, key, map) — the same order as Array.forEach for consistency, which surprises people coming from other languages.

Map vs plain object

A plain object can do most of what a Map does, but Map wins almost every comparison:

  • Keys: Map accepts any type; objects coerce to strings (and symbols).

  • Inherited keys: Map is clean by construction. Objects inherit from Object.prototype, so toString, constructor, __proto__ etc. exist as keys unless you use Object.create(null).

  • Size: map.size is O(1). For an object you have to count Object.keys(obj).length.

  • Iteration: Maps are directly iterable. Objects require Object.keys / entries / values.

  • Performance: Map is optimised for frequent insertion and deletion. Objects are optimised for static shapes.

Use objects for records, Maps for dictionaries
Pick a plain object when the keys are a known, fixed shape (a config, a DTO, a row). Pick a Map when the keys are dynamic, untrusted, or non-string.
Converting between them

JS
const obj = { a: 1, b: 2 };

const mapFromObj = new Map(Object.entries(obj));
const objFromMap = Object.fromEntries(mapFromObj);

console.log(mapFromObj);    // Map(2) { 'a' => 1, 'b' => 2 }
console.log(objFromMap);    // { a: 1, b: 2 }
Common recipes

Counting occurrences

JS
function count(values) {
  const counts = new Map();
  for (const v of values) {
    counts.set(v, (counts.get(v) ?? 0) + 1);
  }
  return counts;
}

console.log(count(["a", "b", "a", "c", "a", "b"]));
// Map(3) { 'a' => 3, 'b' => 2, 'c' => 1 }

Memoising a function

JS
function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

Indexing by id

JS
const byId = new Map(users.map(u => [u.id, u]));
console.log(byId.get(42));    // O(1) lookup
Gotchas
  • map[key] does not work the way you expect — it treats the map like an object and reads through Object.prototype. Always use get/set.

  • Maps are not JSON-serialisable out of the box. JSON.stringify(new Map([["a", 1]])) returns "{}". Convert via Object.fromEntries(map) for string keys.

  • Equality is reference-based for objects. Two { id: 1 } literals are two different keys.

One sentence to remember
Use a `Map` whenever your keys are dynamic, non-string, or you care about predictable iteration order — and reach for a plain object only when you have a fixed shape.