JavaScript Sets & Maps
Set and Map are the collection types you reach for after outgrowing plain arrays and objects. A Set stores unique values; a Map stores key-value pairs with any key type and predictable iteration order. Plus there are weak versions (WeakSet, WeakMap) that play nicely with garbage collection. This chapter covers all of them, with the real-world patterns where they outshine the alternatives.
Set — Unique Values
const s = new Set([1, 2, 3, 2, 1]);
console.log(s.size); // Output: 3
console.log([...s]); // Output: [1, 2, 3]
s.add(4);
s.has(2); // true
s.delete(1);
s.clear();
Sets preserve insertion order and use SameValueZero equality (same as ===, but NaN === NaN is true here).
When to Use a Set
| Task | Set wins because |
|---|---|
| Deduplicate an array | One line: [...new Set(arr)] |
| Membership test | O(1) has vs O(n) Array.includes |
| Track “seen” items in a loop | Built-in, intent-revealing |
| Set algebra (union, intersection) | First-class in ES2025 |
Dedup an Array
const unique = [...new Set([1, 2, 2, 3, 3, 3])];
// Output: [1, 2, 3]
Set Operations (ES2025)
Modern engines support set algebra directly:
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // {1,2,3,4}
a.intersection(b); // {2,3}
a.difference(b); // {1}
a.symmetricDifference(b); // {1,4}
a.isSubsetOf(b); // false
a.isSupersetOf(new Set([1])); // true
a.isDisjointFrom(new Set([9])); // true
If your runtime doesn’t have them yet, the polyfills are tiny.
Iteration
const s = new Set(["a", "b", "c"]);
for (const v of s) console.log(v);
s.forEach(v => console.log(v));
[...s]; // ["a", "b", "c"]
Map — Key-Value Pairs (Any Key)
const m = new Map();
m.set("name", "Ada");
m.set(42, "answer");
m.set(true, "yes");
const key = { id: 1 };
m.set(key, "object key works");
m.get("name"); // "Ada"
m.get(key); // "object key works"
m.has(42); // true
m.size; // 4
m.delete("name");
Map vs Plain Object
Map |
Object |
|
|---|---|---|
| Key types | any (objects, functions, primitives) | strings & symbols only |
| Iteration order | insertion order, guaranteed | mostly insertion order, but quirky for integer-like keys |
| Size | map.size |
Object.keys(o).length (O(n)) |
| Defaults / prototype | none | inherits from Object.prototype (toString, etc.) |
| Iteration | for…of, forEach direct |
needs Object.keys/entries/values |
| Serialisation | not directly JSON | direct JSON |
| Best for | dictionaries with non-string keys, frequent add/remove | configs, DTOs, JSON shapes |
Tip: If you’re treating a plain object as a hashmap (obj[key] = … in a loop, frequent deletes, unknown keys from input), switch to Map. It avoids prototype pollution, supports any key, and is faster for that pattern.
Initialise from Entries
const m = new Map([
["one", 1],
["two", 2],
["three", 3],
]);
Iteration
for (const [k, v] of m) console.log(k, v);
for (const k of m.keys()) console.log(k);
for (const v of m.values()) console.log(v);
for (const [k, v] of m.entries()) console.log(k, v);
m.forEach((v, k) => console.log(k, v));
The default iterator is entries, so the first form is the idiomatic walk.
Convert To / From Object
const obj = Object.fromEntries(m); // Map → object (string/symbol keys only)
const m2 = new Map(Object.entries(obj)); // object → Map
JSON-friendly when the keys are strings.
WeakSet and WeakMap
Same idea, but keys must be objects and are held weakly — the garbage collector can reclaim them even while the set/map exists.
const seen = new WeakSet();
function process(node) {
if (seen.has(node)) return;
seen.add(node);
// …
}
Use cases:
- Tagging DOM nodes / objects without modifying them — when the node is removed, your tracker stops referencing it automatically
- Per-instance metadata in a library
- Caches keyed on object identity that should not leak
Constraints:
- Keys must be objects (no strings/numbers)
- Not iterable — no
size, nofor…of, nokeys() - You can only
add/has/delete(Set) orget/set/has/delete(Map)
const meta = new WeakMap();
class Widget {
constructor() { meta.set(this, { renders: 0 }); }
render() { meta.get(this).renders++; }
}
When the Widget is unreferenced, its meta entry vanishes too — no leak.
A Realistic Example — Grouping with a Map
ES2024 added Object.groupBy / Map.groupBy. Before that you’d write it yourself:
const people = [
{ name: "Ada", team: "core" },
{ name: "Bea", team: "ui" },
{ name: "Cleo", team: "core" },
];
const byTeam = new Map();
for (const p of people) {
if (!byTeam.has(p.team)) byTeam.set(p.team, []);
byTeam.get(p.team).push(p);
}
byTeam.get("core");
// Output: [{name:"Ada", team:"core"}, {name:"Cleo", team:"core"}]
Modern form:
const byTeam = Map.groupBy(people, (p) => p.team);
Map.groupBy is preferred over Object.groupBy when the grouping key might not be a string.
A Realistic Example — Memoisation with Map
function memoize(fn) {
const cache = new Map();
return function (key) {
if (cache.has(key)) return cache.get(key);
const v = fn.call(this, key);
cache.set(key, v);
return v;
};
}
const slowSquare = (n) => { /* expensive */ return n * n; };
const fastSquare = memoize(slowSquare);
For object keys with automatic cleanup, swap Map for WeakMap.
A Realistic Example — Set-Based Filter
const admins = new Set(["ada", "bea", "cleo"]);
const adminUsers = users.filter((u) => admins.has(u.name));
Set.has is O(1) — much faster than array.includes when you call it in a loop.
Performance Notes
| Operation | Object |
Map |
Array.includes |
Set.has |
|---|---|---|---|---|
| Lookup | O(1) avg | O(1) avg | O(n) | O(1) avg |
| Insertion | O(1) | O(1) | O(1) push | O(1) |
| Delete | O(1) (slow path) | O(1) | O(n) splice | O(1) |
| Memory | Lower | Higher per entry | Lowest | Higher per entry |
For a few keys, plain object/array often wins on overhead. For anything growing or hot, Map/Set win clearly.
Common Mistakes
// ❌ Trying to use object keys in plain objects
const m = {};
const k = { id: 1 };
m[k] = "x"; // stringified to "[object Object]"
// ✅ Use Map
const map = new Map();
map.set(k, "x");
// ❌ Forgetting that Set/Map don't JSON-serialise directly
JSON.stringify(new Set([1, 2])); // "{}" — loses data
// ✅ Convert first
JSON.stringify([...mySet]);
JSON.stringify(Object.fromEntries(myMap));
// ❌ Comparing with ===
const s = new Set();
s.add({ a: 1 });
s.has({ a: 1 }); // false — different objects
// ❌ Iterating a WeakMap
for (const x of new WeakMap()) {} // TypeError — not iterable
// ❌ Using Set assuming deep equality
new Set([[1], [1]]).size; // 2 — arrays are distinct objects
Best Practices Checklist
- ✅ Use
Setto deduplicate or to test membership in a hot loop - ✅ Use
Mapwhen keys are objects, integers, or arbitrarily added/removed - ✅ Iterate maps directly with
for (const [k, v] of map) - ✅ Convert to arrays/objects only at serialisation boundaries
- ✅ Use
WeakMap/WeakSetwhen keying on objects whose lifetimes you don’t control - ✅ Prefer
Map.groupBy(ES2024) over hand-written grouping - ❌ Don’t use a plain object as a hashmap when keys come from user input (prototype pollution risk)
- ❌ Don’t expect
JSON.stringifyto handleSet/Mapnatively - ❌ Don’t put primitives in
WeakMap/WeakSet— only objects
What’s Next?
You can pick the right container for any data. Next we go one level deeper into how JavaScript iteration works under the hood: iterators and generators — building your own sequences, lazy evaluation, and for await.
← Previous: JS Regular Expressions Next: JS Iterators & Generators →