Maps
Mental model: A Map is a dictionary whose keys can be anything, whose order you can trust, and whose size you can read.
Level: intermediate · about 11 minutes
const obj = {};
obj[1] = 'number one';
obj['1'] = 'string one';
console.log(Object.keys(obj)); // ['1'], one key, the second overwrote the first
const map = new Map();
map.set(1, 'number one');
map.set('1', 'string one');
console.log(map.size); // 2, two distinct keysA plain object turns every key into a string. A Map does not.
A **Map** stores key-value pairs where the key can be any value at all: a number, an object, a function, even NaN. A plain object coerces every key to a string (or a symbol), which is fine for JSON-shaped data and wrong the moment you want to key by object identity.
const scores = new Map();
scores.set('ada', 10).set('grace', 12); // set returns the Map, so it chains
console.log(scores.get('ada')); // 10
console.log(scores.get('nobody')); // undefined
console.log(scores.has('grace')); // true
console.log(scores.delete('ada')); // true
console.log(scores.size); // 1
scores.clear();
console.log(scores.size); // 0
const config = new Map([
['theme', 'dark'],
['fontSize', 14],
]);
console.log([...config]); // [['theme','dark'], ['fontSize',14]]
console.log(Object.fromEntries(config)); // { theme: 'dark', fontSize: 14 }
console.log(new Map(Object.entries({ a: 1 })).get('a')); // 1
Object keys are the real reason Maps exist
const ada = { name: 'ada' };
const alan = { name: 'alan' };
const lastSeen = new Map();
lastSeen.set(ada, '2024-01-01');
lastSeen.set(alan, '2024-02-01');
console.log(lastSeen.get(ada)); // '2024-01-01'
console.log(lastSeen.get({ name: 'ada' })); // undefined, different object
const broken = {};
broken[ada] = 'x';
console.log(Object.keys(broken)); // ['[object Object]'], every object collides
Iteration you can rely on
const stock = new Map([['pen', 3], ['pad', 0], ['ink', 7]]);
for (const [item, qty] of stock) console.log(item, qty); // insertion order
console.log([...stock.keys()]); // ['pen', 'pad', 'ink']
console.log([...stock.values()]); // [3, 0, 7]
console.log([...stock.entries()][0]); // ['pen', 3]
const inStock = new Map([...stock].filter(([, qty]) => qty > 0));
console.log(inStock.size); // 2, array methods via spread
const obj = { 10: 'ten', 2: 'two', b: 'bee', a: 'ay' };
console.log(Object.keys(obj)); // ['2', '10', 'b', 'a'], numbers reordered
const map = new Map([[10, 'ten'], [2, 'two'], ['b', 'bee']]);
console.log([...map.keys()]); // [10, 2, 'b'], exactly as inserted
Map or plain object?
| If you need | Use | Why |
|---|---|---|
| keys that are not strings | Map | objects, numbers and functions stay themselves |
| guaranteed insertion order | Map | objects reorder integer-like keys |
| a count | Map | map.size versus Object.keys(obj).length |
| frequent adds and deletes | Map | built for it, and no prototype surprises |
| JSON in or out | object | JSON.stringify(map) gives you {} |
| fixed, known fields | object | destructuring, shorthand, familiarity |
| methods alongside data | object or class | a Map holds values, not behaviour |
| a config read many times | object | property access is heavily optimised |
const votes = ['a', 'b', 'a', 'c', 'a'];
const tally = new Map();
for (const v of votes) tally.set(v, (tally.get(v) ?? 0) + 1);
console.log([...tally]); // [['a', 3], ['b', 1], ['c', 1]]
// No inherited keys to worry about: a vote for 'toString' is just a key.Counting with a Map, the honest version of the reduce-into-object trick.
A first look at WeakMap
A WeakMap takes object keys only and holds them weakly: if nothing else in your program references the key object, the entry can be garbage collected. It is the right tool for attaching data to objects you do not own, such as DOM nodes or instances from a library.
const meta = new WeakMap();
let node = { tag: 'div' };
meta.set(node, { clicks: 0 });
console.log(meta.get(node)); // { clicks: 0 }
node = null; // the entry becomes collectable, no cleanup call needed
// A WeakMap has no size, no keys() and is not iterable: you cannot observe collection.
const m = new Map();
m.set(NaN, 'not a number');
m.set('1', 'string');
m.set(1, 'number');
console.log(m.size, m.get(NaN));Map keys use SameValueZero, so NaN works as a key and finds itself again. "1" and 1 are different keys because there is no string coercion, giving three entries in total.
Try it yourself
Map versus object, side by side
const asObject = {};
const asMap = new Map();
const key = { id: 7 };
asObject[key] = 'object key';
asMap.set(key, 'object key');
console.log(Object.keys(asObject)); // ['[object Object]']
console.log(asMap.get(key)); // 'object key'
console.log('toString' in asObject); // true, inherited!
console.log(asMap.has('toString')); // false, Maps have no inherited keys
console.log(JSON.stringify(asMap)); // {}
console.log(JSON.stringify([...asMap.entries()].length)); // 1
Add a key named toString to both and compare has/in results. Then try to JSON.stringify each one.
Exercises
toLookup
Write toLookup(items, key) that returns a Map from item[key] to the item itself, so you can fetch a record by id without scanning. When two items share a key, the last one wins. An empty list gives an empty Map.
Check yourself
- What does this log?
- '{}' 1 — A Map stores entries in internal slots, not own properties, so
JSON.stringifyfinds nothing to serialise and produces{}. Convert withObject.fromEntries(map)or[...map]before stringifying. - You need to associate metadata with DOM nodes, and the nodes come and go. Which collection?
- A
WeakMap— AWeakMapkeyed by the node lets the entry disappear when the node does, so removing an element cannot leak its metadata. A regularMapwould keep every detached node alive. - Which statement about ordering is true?
- A Map iterates in insertion order; an object lists integer-like keys first in numeric order —
{ 2: "b", 1: "a" }iterates1then2, because integer-like keys are ordered numerically before the string keys. A Map keeps whatever order you inserted, which is why it is the safer choice for ordered data.
Common mistakes
- Using
map.lengthor bracket access. It ismap.sizeandmap.get(key). JSON.stringifyon a Map, which silently gives you{}.- Keying a plain object by an object, which stringifies to
"[object Object]"and collides.
Takeaways
- Map keys can be any value, compared with SameValueZero, with no string coercion.
- Iteration is always insertion order, and
sizeis a property away. - Convert with
[...map],Object.fromEntries(map)andnew Map(Object.entries(obj)). - Reach for
WeakMapwhen the key is an object whose lifetime you do not control.