Maps, Sets & Weak Collections
Before ES6, plain objects did double duty as dictionaries, with string-only keys and prototype surprises. Map and Set are the proper tools. ES2024 and ES2025 added grouping and set algebra, and the weak collections let the garbage collector clean up after you.
You will learn
Map: any key type, ordered, sized, iterable ES2015, and when to choose it over an object.Set: uniqueness, plus the new set operationsunion,intersectionand friends ES2025.- Grouping with
Object.groupByandMap.groupByES2024. - Upserts with
getOrInsert/getOrInsertComputed(new in 2026 engines). WeakMap,WeakSetandWeakRef: collections that don't keep their keys alive.
Map ES2015
Map or plain object?
Map | Object | |
|---|---|---|
| Key types | anything: objects, numbers, NaN, functions | strings and symbols only (numbers become strings) |
| Order | insertion order, always | integer-like keys first (ascending), then insertion order (Chapter 10) |
| Size | map.size | Object.keys(o).length |
| Iteration | directly iterable | via Object.entries |
| Accidental keys | none | inherited ones like "toString", unless created with Object.create(null) |
| JSON | needs converting | native |
| Best for | dynamic dictionaries, caches, frequent add/remove | fixed-shape records, JSON data |
β οΈ Pitfall: map["b1"] = 3 sets an ordinary property on the Map object, not an entry: get("b1") won't see it and size doesn't change. Always use set and get.
Map keys use SameValueZero (Chapter 5): NaN works as a key, and 0 and -0 are the same key.
β Java: a Map behaves like LinkedHashMap (it keeps insertion order). But object keys are compared by identity; there's no hashCode()/equals() override, so two equal-looking objects are two different keys.
Upsert: getOrInsert and getOrInsertComputed
"Get the value, or insert a default first" is extremely common. Newer engines (Node 26 included) ship getOrInsert(key, value) and getOrInsertComputed(key, fn), where the function only runs if the key is missing.
π Availability: getOrInsert is very new. Check your target runtimes; the has/set/get pattern works everywhere.
Set ES2015
A Set holds unique values, in insertion order, and checks membership quickly.
Set operations ES2025
| Method | Result | Maths |
|---|---|---|
a.union(b) | in a or b | a βͺ b |
a.intersection(b) | in both | a β© b |
a.difference(b) | in a, not in b | a β b |
a.symmetricDifference(b) | in exactly one | a Ξ b |
a.isSubsetOf(b), isSupersetOf, isDisjointFrom | booleans | β, β, no overlap |
These methods return new sets and never change the originals. The argument may be any "set-like" object with size, has and keys, so a Map works too (its keys are used).
π Older syntax you'll still see: new Set([...a].filter((x) => b.has(x))) for intersection, and similar spreads for the others. They still work, but the new methods are clearer and faster.
Grouping ES2024
Object.groupBy(items, fn) returns an object whose keys are the strings fn returns. Map.groupBy returns a Map, so keys can be any value.
β οΈ Pitfall: the object from Object.groupBy has a null prototype, so it has no hasOwnProperty method, and Node prints it as [Object: null prototype] {β¦}. Use Object.hasOwn(groups, key) or key in groups.
Weak collections
A normal Map keeps its keys alive forever. A WeakMap holds its keys weakly: if nothing else references a key object, the garbage collector may remove the entry. In exchange, keys must be objects (or non-registered symbols), and weak collections can't be iterated or sized, since their contents may vanish at any moment.
| Map / Set | WeakMap / WeakSet | |
|---|---|---|
| Keys | any value | objects (and non-registered symbols) only |
Iterable, size | yes | no |
| Keeps keys alive | yes | no |
| Typical use | data you own | metadata attached to objects you don't own: caches, "seen" markers, private data |
WeakRef and FinalizationRegistry ES2021 go one step further: they let you hold a single object weakly and get notified after it's collected. They're advanced tools with unpredictable timing; Chapter 15 covers them.
More examples
1. Counting with a Map
2. Removing duplicate objects by id
3. Converting between Map and JSON
4. Permissions with Set operations
5. Grouping orders by customer and summing
Recap
- Use
Mapfor dynamic dictionaries: any key type, insertion order,size, direct iteration. Useget/set, never brackets. - Use
Setfor uniqueness and fast membership. ES2025 addsunion,intersection,differenceand the subset checks. Object.groupByreturns a null-prototype object;Map.groupBysupports any key type.getOrInsertComputedhandles "get or create" in one call, where it's available.WeakMap/WeakSettake object keys only, aren't iterable, and don't prevent garbage collection, which makes them ideal for metadata and caches.
Exercises
Exercise 1 π’ easy build it
Remove duplicates from ["Dune", "Emma", "Dune", "Hamlet", "Emma"], keeping the first-seen order.
Show solution
A Set keeps insertion order and ignores repeated adds.
Exercise 2 π’ easy predict the output
What does this print?
Show solution
2 number undefined. 1 and "1" are different Map keys. The bracket assignment created an ordinary property, not an entry.
Exercise 3 π’ easy fix the bug
This always prints undefined. Why? Fix it.
Show solution
Object keys compare by identity: the second { id: "b1" } is a different object.
Exercise 4 π’ easy build it
Given two sets of book ids, list the ids that are in the cart but no longer in the catalog.
Show solution
difference keeps the items of the first set that aren't in the second.
Exercise 5 π‘ medium refactor it
Replace this manual grouping with Object.groupBy.
Show solution
groupBy groups whole items; a small map step turns them into titles. (Copying with Object.fromEntries also gives a normal prototype.)
Exercise 6 π‘ medium explain why
Why can't you loop over a WeakMap or read its size?
Show solution
Entries can disappear whenever the garbage collector runs, which happens at unpredictable times. Iteration or a size would expose GC timing and make programs non-deterministic, so the API only offers get/set/has/delete for a key you already hold.
Exercise 7 π‘ medium build it Β· playground
Create playground/inventory-map.js with a class-free inventory built on a Map: add(id, qty), remove(id, qty) (throwing if there isn't enough stock), lowStock(limit), which returns the ids at or below the limit, and toJSON().
Show solution
The Map is private, held in a closure (Chapter 4). JSON.stringify calls toJSON automatically, and returning this allows chaining.
Exercise 8 π‘ medium predict the output
What does this print?
Show solution
[ 1, 2, 4 ] 3 true false. The set methods return new sets and never change a.
Exercise 9 π΄ hard build it
Write memoizeByObject(fn), which caches results per argument object in a WeakMap, and per primitive argument in a Map.
Show solution
Objects go into the WeakMap, so cached results disappear together with their keys. Primitives can't be WeakMap keys, so they use a normal Map.
Exercise 10 π΄ hard mini-project Β· playground
Create playground/recommend.js. Given each customer's purchased book ids, recommend for a customer the books bought by others who share at least one purchase with them, excluding books they already own, ranked by how many such customers bought each one. Use Sets for the purchases, the Set methods, and a Map for the scores.
Show solution
isDisjointFrom finds customers with something in common, and difference keeps only books Ana doesn't own. A Map counts the scores, and the sort uses the score and then the id for a stable, predictable order.