Map, Set and WeakMap: when a plain object is the wrong tool
Why an object makes a poor dictionary, what Map and Set guarantee about order and keys, and what WeakMap is for. Then an LRU cache built on Map's order.
After this lesson you can
- Explain three ways a plain object misbehaves as a dictionary
- Use Set for uniqueness and set operations, knowing how it compares values
- Say what WeakMap can and cannot do, and when to reach for it
- Build an LRU cache on Map's insertion order
A plain object works as a lookup table until the keys come from data you do not control. Then three things go wrong:
- Inherited keys.
{}inherits fromObject.prototype, solookup["constructor"]already exists — it is theObjectfunction — and"constructor" in lookupistruebefore anything was stored. - Keys become strings.
obj[1]andobj["1"]are the same entry, and every object used as a key becomes"[object Object]". - Order is not insertion order. Integer-like keys (
"2","10") are listed first, in numeric order, before every other key.
A Map has none of these problems. Keys can be any value — objects,
numbers, NaN — and are compared by SameValueZero (like ===, except
NaN equals NaN). Nothing is inherited. Iteration follows insertion
order exactly, and size is a property, not a count you compute.
export function run() { const words = ["b", "constructor", "a", "10", "2", "a"]; const firstAt = {}; words.forEach((w, i) => { if (!(w in firstAt)) firstAt[w] = i; }); const map = new Map(); words.forEach((w, i) => { if (!map.has(w)) map.set(w, i); }); const byObject = {}; byObject[{ id: 1 }] = "first"; byObject[{ id: 2 }] = "second"; const numberAndString = new Map([[1, "number"], ["1", "string"]]); return { objectKeyOrder: Object.keys(firstAt), objectConstructorEntry: typeof firstAt.constructor, mapKeyOrder: [...map.keys()], mapConstructorEntry: map.get("constructor"), mapSize: map.size, objectKeysFromObjects: Object.keys(byObject), mapKeepsNumberAndString: numberAndString.size, };}An object is still the right choice for a record with fixed, known
field names — a user, a config — and it serialises to JSON directly,
which a Map does not.
Set
A Set holds each value once, compared the same way as Map keys.
[...new Set(items)] is the idiomatic de-duplication, and it keeps the
first occurrence's order. Two objects with the same contents are still
two different values: a Set compares references, not shapes. Recent
engines, Node 22 included, also have set algebra built in: union,
intersection, difference, symmetricDifference, isSubsetOf,
isSupersetOf and isDisjointFrom.
WeakMap
A WeakMap only accepts objects (and non-registered symbols) as keys
and holds them weakly: if nothing else references the key object,
it can be garbage-collected and its entry disappears. The price is that
a WeakMap cannot be iterated and has no size — its contents depend
on the garbage collector. Use it to attach data to objects you do not
own, such as caching a computed value per DOM node or per request
object, without keeping those objects alive. WeakSet is the same idea
for membership.
export function run() { const team = new Set(["ana", "gio", "nino"]); const onCall = new Set(["gio", "lika"]); const meta = new WeakMap(); const request = { url: "/home" }; meta.set(request, { startedAt: "t0" }); let primitiveKey; try { meta.set("/home", {}); } catch (e) { primitiveKey = `${e.name}: ${e.message}`; } return { unique: [...new Set([3, 1, 3, 2, 1])], nanOnce: new Set([NaN, NaN]).size, sameShapeObjects: new Set([{ id: 1 }, { id: 1 }]).size, inExactlyOne: [...team.symmetricDifference(onCall)], onCallIsSubset: onCall.isSubsetOf(team), noOverlapWithInterns: team.isDisjointFrom(new Set(["sandro"])), weakGet: meta.get(request), primitiveKey, weakSize: typeof meta.size, weakIterable: typeof meta[Symbol.iterator], };}Map order makes an LRU cache easy
A least-recently-used cache evicts the entry that was used longest ago.
Because a Map iterates in insertion order, map.keys().next().value
is the oldest key; to mark a key as just used, delete it and set it
again so it moves to the end. That is the exercise.
Common mistakes
- A plain object as a lookup table for data keys. Keys such as
"constructor"or"toString"already exist on it, as the first example shows. - Expecting
Setto remove duplicate objects.{ id: 1 }twice is two entries; de-duplicate by a key (new Map(items.map((x) => [x.id, x]))). map[key] = valueon a Map. That sets an ordinary property on the Map object;get,hasandsizenever see it. Useset.- Updating a Map entry and expecting it to move.
seton an existing key keeps its original position; delete it first.
What interviewers ask
Exercise · Pro
An LRU cache on a Map
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade