JavaScript Set & Map
JavaScript provides two powerful collection types beyond plain objects and arrays: Map and Set. Both were introduced in ES6 and solve specific problems that plain objects and arrays handle poorly. Map is a key-value store that accepts any type as a key, while Set stores unique values of any type.
Unlike plain objects, Maps preserve insertion order, have a size property, and support efficient iteration. Unlike arrays, Sets enforce uniqueness automatically and provide O(1) membership testing. Together, these collections form the foundation for caches, deduplication, metadata storage, and many other patterns.
This guide covers Map, Set, WeakMap, and WeakSet in depth, with practical examples and guidance on when to use each type.
| 1 | // Quick overview |
| 2 | const map = new Map(); // Key-value pairs (any key type) |
| 3 | const set = new Set(); // Unique values |
| 4 | const weakMap = new WeakMap(); // GC-friendly key-value (object keys only) |
| 5 | const weakSet = new WeakSet(); // GC-friendly membership (object keys only) |
| 6 | |
| 7 | // Map vs Object |
| 8 | const obj = { a: 1, b: 2 }; // String keys only |
| 9 | const mp = new Map([["a", 1], ["b", 2]]); // Any keys |
| 10 | |
| 11 | // Set vs Array |
| 12 | const arr = [1, 2, 2, 3]; // Duplicates allowed |
| 13 | const st = new Set([1, 2, 2, 3]); // Duplicates removed |
A Map holds key-value pairs where keys can be any JavaScript type — objects, functions, primitives, even NaN. Maps remember insertion order, provide O(1) lookups, and expose a size property for instant length checks.
Map Methods
| Method | Description |
|---|---|
| set(key, value) | Add or update an entry (chainable) |
| get(key) | Retrieve value, undefined if missing |
| has(key) | Check if key exists |
| delete(key) | Remove entry (returns boolean) |
| clear() | Remove all entries |
| size | Number of entries (property) |
| forEach(cb) | Iterate in insertion order |
| 1 | // Basic Map operations |
| 2 | const userMap = new Map(); |
| 3 | |
| 4 | // Keys can be any type |
| 5 | userMap.set("name", "Alice"); |
| 6 | userMap.set(42, "the answer"); |
| 7 | userMap.set(true, "boolean key"); |
| 8 | userMap.set(NaN, "NaN is a valid key"); |
| 9 | |
| 10 | console.log(userMap.get("name")); // "Alice" |
| 11 | console.log(userMap.get(42)); // "the answer" |
| 12 | console.log(userMap.get(NaN)); // "NaN is a valid key" |
| 13 | console.log(userMap.size); // 4 |
| 14 | console.log(userMap.has("name")); // true |
| 15 | |
| 16 | // Chain set() calls |
| 17 | userMap.set("admin", true) |
| 18 | .set("role", "editor") |
| 19 | .set("active", true); |
| 20 | |
| 21 | // Iteration in insertion order |
| 22 | for (const [key, value] of userMap) { |
| 23 | console.log(key, value); |
| 24 | } |
| 25 | |
| 26 | // Convert to/from arrays |
| 27 | const entries = [...userMap]; // Array of [key, value] pairs |
| 28 | const restored = new Map(entries); // Restore from array |
| 29 | |
| 30 | // Object to Map and back |
| 31 | const obj = { a: 1, b: 2, c: 3 }; |
| 32 | const fromObj = new Map(Object.entries(obj)); |
| 33 | const backToObj = Object.fromEntries(fromObj); |
| 1 | // Practical: memoization with Map |
| 2 | function memoize(fn) { |
| 3 | const cache = new Map(); |
| 4 | return function (...args) { |
| 5 | const key = JSON.stringify(args); |
| 6 | if (cache.has(key)) { |
| 7 | return cache.get(key); |
| 8 | } |
| 9 | const result = fn.apply(this, args); |
| 10 | cache.set(key, result); |
| 11 | return result; |
| 12 | }; |
| 13 | } |
| 14 | |
| 15 | const fibonacci = memoize((n) => { |
| 16 | if (n <= 1) return n; |
| 17 | return fibonacci(n - 1) + fibonacci(n - 2); |
| 18 | }); |
| 19 | |
| 20 | console.log(fibonacci(100)); // 354224848179262000000 — fast with cache |
| 21 | |
| 22 | // Practical: counting occurrences |
| 23 | function countWords(text) { |
| 24 | const counts = new Map(); |
| 25 | const words = text.toLowerCase().split(/\s+/); |
| 26 | |
| 27 | for (const word of words) { |
| 28 | counts.set(word, (counts.get(word) || 0) + 1); |
| 29 | } |
| 30 | |
| 31 | return counts; |
| 32 | } |
| 33 | |
| 34 | const wordCount = countWords("hello world hello javascript"); |
| 35 | console.log([...wordCount.entries()]); |
| 36 | // [["hello", 2], ["world", 1], ["javascript", 1]] |
Both Map and Object store key-value pairs, but they serve different purposes. Choose Object for fixed string keys known at author time. Choose Map for dynamic keys, especially non-string types, frequent additions/deletions, or when you need reliable size tracking.
| Feature | Map | Object |
|---|---|---|
| Key types | Any | Strings & Symbols only |
| Order | Insertion order | Integer keys first |
| size | O(1) via .size | O(n) via Object.keys() |
| Iteration | Directly iterable | Via Object.keys/values/entries |
| JSON | No direct support | Native JSON support |
best practice
A Set stores unique values of any type. It uses SameValueZero equality — each value can appear only once. Sets are ideal for deduplication, membership testing, and as building blocks for set operations.
Set Methods
| Method | Description |
|---|---|
| add(value) | Add value (returns the Set for chaining) |
| has(value) | Check if value exists |
| delete(value) | Remove value (returns boolean) |
| clear() | Remove all values |
| size | Number of values |
| forEach(cb) | Iterate in insertion order |
| 1 | // Basic Set operations |
| 2 | const set = new Set(); |
| 3 | |
| 4 | set.add(1); |
| 5 | set.add(2); |
| 6 | set.add(3); |
| 7 | set.add(1); // Ignored — already exists |
| 8 | |
| 9 | console.log(set.size); // 3 |
| 10 | console.log(set.has(2)); // true |
| 11 | console.log(set.has(4)); // false |
| 12 | |
| 13 | set.delete(2); |
| 14 | console.log(set.has(2)); // false |
| 15 | |
| 16 | // Initialize from iterable |
| 17 | const fromArray = new Set([1, 2, 2, 3, 3, 4]); |
| 18 | console.log(fromArray.size); // 4 — duplicates removed |
| 19 | console.log([...fromArray]); // [1, 2, 3, 4] |
| 20 | |
| 21 | // Iteration in insertion order |
| 22 | for (const val of set) { |
| 23 | console.log(val); |
| 24 | } |
| 25 | |
| 26 | // Practical: deduplicate arrays |
| 27 | const duplicates = [1, 2, 2, 3, 4, 4, 5]; |
| 28 | const unique = [...new Set(duplicates)]; // [1, 2, 3, 4, 5] |
| 29 | |
| 30 | // Practical: check all values unique |
| 31 | function allUnique(arr) { |
| 32 | return new Set(arr).size === arr.length; |
| 33 | } |
| 34 | console.log(allUnique([1, 2, 3])); // true |
| 35 | console.log(allUnique([1, 2, 2])); // false |
| 1 | // Set operations (union, intersection, difference) |
| 2 | function union(a, b) { |
| 3 | return new Set([...a, ...b]); |
| 4 | } |
| 5 | |
| 6 | function intersection(a, b) { |
| 7 | return new Set([...a].filter(x => b.has(x))); |
| 8 | } |
| 9 | |
| 10 | function difference(a, b) { |
| 11 | return new Set([...a].filter(x => !b.has(x))); |
| 12 | } |
| 13 | |
| 14 | function symmetricDifference(a, b) { |
| 15 | return new Set([ |
| 16 | ...[...a].filter(x => !b.has(x)), |
| 17 | ...[...b].filter(x => !a.has(x)) |
| 18 | ]); |
| 19 | } |
| 20 | |
| 21 | // Usage |
| 22 | const A = new Set([1, 2, 3, 4, 5]); |
| 23 | const B = new Set([4, 5, 6, 7, 8]); |
| 24 | |
| 25 | console.log([...union(A, B)]); // [1,2,3,4,5,6,7,8] |
| 26 | console.log([...intersection(A, B)]); // [4, 5] |
| 27 | console.log([...difference(A, B)]); // [1, 2, 3] |
| 28 | console.log([...symmetricDifference(A, B)]); // [1,2,3,6,7,8] |
| 29 | |
| 30 | // Practical: permission checking |
| 31 | const userPermissions = new Set(["read", "write", "delete"]); |
| 32 | const requiredPermissions = new Set(["read", "write"]); |
| 33 | |
| 34 | function hasAllPermissions(user, required) { |
| 35 | return [...required].every(p => user.has(p)); |
| 36 | } |
| 37 | |
| 38 | console.log(hasAllPermissions(userPermissions, requiredPermissions)); // true |
info
A WeakMap is like a Map but its keys are held weakly. If there are no other references to a key object, both the key and its value are eligible for garbage collection. This makes WeakMaps perfect for private data, metadata attached to objects, and caches without memory leaks.
WeakMaps have constraints: keys must be objects (not primitives), there is no size property or iteration methods, and they cannot be cleared. These constraints exist because contents could change at any time due to garbage collection.
| 1 | // WeakMap basics — object keys only |
| 2 | const weakMap = new WeakMap(); |
| 3 | |
| 4 | let user = { id: 1, name: "Alice" }; |
| 5 | weakMap.set(user, "session-token-abc123"); |
| 6 | |
| 7 | console.log(weakMap.get(user)); // "session-token-abc123" |
| 8 | console.log(weakMap.has(user)); // true |
| 9 | |
| 10 | // When user is dereferenced, the entry can be GC'd |
| 11 | user = null; |
| 12 | |
| 13 | // Private data with WeakMap |
| 14 | const _private = new WeakMap(); |
| 15 | |
| 16 | class Person { |
| 17 | constructor(name, ssn) { |
| 18 | _private.set(this, { ssn }); |
| 19 | this.name = name; |
| 20 | } |
| 21 | |
| 22 | getSSN() { |
| 23 | return _private.get(this).ssn; |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | const person = new Person("Alice", "123-45-6789"); |
| 28 | console.log(person.name); // "Alice" |
| 29 | console.log(person.ssn); // undefined — not accessible |
| 30 | console.log(person.getSSN()); // "123-45-6789" — via privileged method |
| 1 | // Practical: DOM node metadata without memory leaks |
| 2 | const nodeData = new WeakMap(); |
| 3 | |
| 4 | function trackElement(el) { |
| 5 | nodeData.set(el, { |
| 6 | created: Date.now(), |
| 7 | clickCount: 0, |
| 8 | data: {} |
| 9 | }); |
| 10 | } |
| 11 | |
| 12 | document.querySelectorAll(".tracked").forEach((el) => { |
| 13 | trackElement(el); |
| 14 | el.addEventListener("click", () => { |
| 15 | const data = nodeData.get(el); |
| 16 | data.clickCount++; |
| 17 | console.log(`Clicked ${data.clickCount} times`); |
| 18 | }); |
| 19 | }); |
| 20 | // When element is removed from DOM, WeakMap entry |
| 21 | // automatically becomes eligible for garbage collection |
| 22 | |
| 23 | // Practical: caching on objects |
| 24 | const cache = new WeakMap(); |
| 25 | |
| 26 | function computeExpensive(obj) { |
| 27 | if (cache.has(obj)) { |
| 28 | return cache.get(obj); |
| 29 | } |
| 30 | const result = expensiveComputation(obj); |
| 31 | cache.set(obj, result); |
| 32 | return result; |
| 33 | } |
| 34 | // When obj is GC'd, cache entry is cleaned up — no leak! |
pro tip
WeakSet is the set analog of WeakMap. It stores unique objects with weak references — if the object is otherwise unreferenced, it is removed from the set. WeakSet has no iteration, no size, and only accepts object values. It is useful for tagging or marking objects without preventing garbage collection.
| 1 | // WeakSet basics |
| 2 | const visited = new WeakSet(); |
| 3 | |
| 4 | let page1 = { url: "/home" }; |
| 5 | let page2 = { url: "/about" }; |
| 6 | |
| 7 | visited.add(page1); |
| 8 | visited.add(page2); |
| 9 | |
| 10 | console.log(visited.has(page1)); // true |
| 11 | page1 = null; // page1 removed from visited automatically when GC'd |
| 12 | |
| 13 | // Practical: prevent double-processing |
| 14 | const processed = new WeakSet(); |
| 15 | |
| 16 | function processItem(item) { |
| 17 | if (processed.has(item)) { |
| 18 | return; // skip already processed |
| 19 | } |
| 20 | // ... process item ... |
| 21 | processed.add(item); |
| 22 | } |
| 23 | |
| 24 | const items = [{ id: 1 }, { id: 2 }, { id: 1 }]; |
| 25 | items.forEach(processItem); // id:1 processed once, id:2 processed once |
| 26 | |
| 27 | // Practical: marking objects for special handling |
| 28 | const isAuthorized = new WeakSet(); |
| 29 | |
| 30 | function grantAccess(user) { |
| 31 | isAuthorized.add(user); |
| 32 | } |
| 33 | |
| 34 | function checkAccess(user) { |
| 35 | return isAuthorized.has(user); |
| 36 | } |
info
Choosing the right collection type depends on your specific needs. Here is a decision guide to help you pick the right tool for the job.
| 1 | // Decision guide |
| 2 | |
| 3 | // Use Map when: |
| 4 | // 1. Dynamic keys, especially non-string |
| 5 | const userRoles = new Map(); |
| 6 | userRoles.set(userObject, "admin"); |
| 7 | |
| 8 | // 2. Frequent additions/removals |
| 9 | const cache = new Map(); |
| 10 | cache.set("key", value); |
| 11 | cache.delete("key"); |
| 12 | |
| 13 | // 3. Need reliable iteration or size |
| 14 | console.log(cache.size); |
| 15 | for (const [k, v] of cache) { /* ... */ } |
| 16 | |
| 17 | // Use Object when: |
| 18 | // 1. Fixed string keys known at author time |
| 19 | const config = { host: "localhost", port: 3000 }; |
| 20 | |
| 21 | // 2. JSON serialization needed |
| 22 | const json = JSON.stringify(config); |
| 23 | |
| 24 | // 3. Destructuring pattern |
| 25 | const { host, port } = config; |
| 26 | |
| 27 | // Use Set when: |
| 28 | // 1. Need unique values |
| 29 | const unique = [...new Set(array)]; |
| 30 | |
| 31 | // 2. Fast membership testing |
| 32 | const lookup = new Set(ids); |
| 33 | if (lookup.has(target)) { /* O(1) */ } |
| 34 | |
| 35 | // Use WeakMap when: |
| 36 | // 1. Private data attached to objects |
| 37 | // 2. Caches keyed on objects (auto-cleanup) |
| 38 | // 3. DOM element metadata (no memory leaks) |
| 39 | |
| 40 | // Use WeakSet when: |
| 41 | // 1. Marking objects (visited, processed) |
| 42 | // 2. Membership without preventing GC |
warning