JavaScript — Maps, Sets & WeakRefs
Modern JavaScript provides rich collection types beyond plain objects and arrays. Map and Set were introduced in ES6 to address the limitations of using objects as maps and arrays as sets. WeakMap and WeakSet enable memory-efficient associations where keys can be garbage-collected. WeakRef and FinalizationRegistry (ES2021) provide low-level access to the garbage collector's behavior.
These collections are not just convenience APIs — they offer real performance and correctness advantages. Maps preserve insertion order, accept any key type, and have predictable O(1) lookups. Sets enforce uniqueness automatically. Weak collections enable patterns like caching and metadata without preventing garbage collection.
Choosing the right collection type is a key engineering decision. This guide covers each type in depth, with practical patterns, performance benchmarks, and decision criteria for choosing between them.
| 1 | // Overview of collection types |
| 2 | const map = new Map(); // Any-key key-value store |
| 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 | const ref = new WeakRef(target); // Weak reference to an object |
| 7 | const registry = new FinalizationRegistry(callback); // Cleanup notification |
| 8 | |
| 9 | // Side-by-side comparison |
| 10 | const obj = { key: "value" }; // Traditional object (string-only keys) |
| 11 | const mp = new Map([["key", "value"]]); // Map (any keys) |
| 12 | const st = new Set([1, 2, 3]); // Set (unique values) |
| 13 | |
| 14 | console.log(obj.key); // "value" |
| 15 | console.log(mp.get("key")); // "value" |
| 16 | console.log(st.has(2)); // true |
A Map holds key-value pairs where keys can be any JavaScript type — objects, functions, primitives, even NaN. Unlike plain objects, Maps remember insertion order, have a size property, and provide predictable performance for all operations.
Map API
| Method/Property | Description |
|---|---|
| set(key, value) | Add or update an entry (chainable) |
| get(key) | Retrieve value, returns undefined if missing |
| has(key) | Check if key exists (boolean) |
| delete(key) | Remove entry (returns boolean success) |
| clear() | Remove all entries |
| size | Number of entries (property, not method) |
| keys() | Iterator over keys |
| values() | Iterator over values |
| entries() | Iterator over [key, value] pairs |
| forEach(cb) | Iterate in insertion order |
| 1 | // Basic Map operations |
| 2 | const userMap = new Map(); |
| 3 | |
| 4 | // Keys can be any type |
| 5 | const key1 = { id: 1 }; |
| 6 | const key2 = "stringKey"; |
| 7 | const key3 = 42; |
| 8 | const key4 = NaN; // NaN works as a key (per spec, NaN === NaN in Map) |
| 9 | |
| 10 | userMap.set(key1, "Alice"); |
| 11 | userMap.set(key2, "Bob"); |
| 12 | userMap.set(key3, "Charlie"); |
| 13 | userMap.set(key4, "NaN value"); |
| 14 | |
| 15 | console.log(userMap.get(key1)); // "Alice" |
| 16 | console.log(userMap.get(key2)); // "Bob" |
| 17 | console.log(userMap.get(42)); // "Charlie" |
| 18 | console.log(userMap.get(NaN)); // "NaN value" — NaN is treated equal to itself |
| 19 | |
| 20 | console.log(userMap.has(key1)); // true |
| 21 | console.log(userMap.size); // 4 |
| 22 | |
| 23 | // Chain set calls |
| 24 | userMap.set("admin", true).set("role", "editor").set("active", true); |
| 25 | |
| 26 | // Iteration (insertion order) |
| 27 | for (const [key, value] of userMap) { |
| 28 | console.log(key, value); |
| 29 | } |
| 30 | |
| 31 | // Convert to/from arrays |
| 32 | const arr = [...userMap]; // Array of [key, value] pairs |
| 33 | const restored = new Map(arr); // Restore from array |
| 34 | |
| 35 | // Object to Map |
| 36 | const obj = { a: 1, b: 2, c: 3 }; |
| 37 | const objMap = new Map(Object.entries(obj)); // Map(3) {"a" => 1, "b" => 2, "c" => 3} |
| 38 | |
| 39 | // Map to Object |
| 40 | const fromMap = Object.fromEntries(objMap); // { a: 1, b: 2, c: 3 } |
| 1 | // Object keys and reference equality |
| 2 | const map = new Map(); |
| 3 | |
| 4 | const alice = { name: "Alice" }; |
| 5 | const bob = { name: "Bob" }; |
| 6 | |
| 7 | map.set(alice, "admin"); |
| 8 | map.set(bob, "user"); |
| 9 | |
| 10 | // Map uses SameValueZero equality for keys |
| 11 | // For objects, this means reference equality |
| 12 | console.log(map.get({ name: "Alice" })); // undefined — different reference |
| 13 | console.log(map.get(alice)); // "admin" — same reference |
| 14 | |
| 15 | // Practical: DOM element metadata |
| 16 | const buttonMeta = new Map(); |
| 17 | |
| 18 | document.querySelectorAll("button").forEach((btn) => { |
| 19 | buttonMeta.set(btn, { |
| 20 | clickCount: 0, |
| 21 | lastClicked: null, |
| 22 | handler: () => { |
| 23 | const meta = buttonMeta.get(btn); |
| 24 | meta.clickCount++; |
| 25 | meta.lastClicked = Date.now(); |
| 26 | } |
| 27 | }); |
| 28 | btn.addEventListener("click", buttonMeta.get(btn).handler); |
| 29 | }); |
| 30 | |
| 31 | // Practical: memoization with Map |
| 32 | function memoize(fn) { |
| 33 | const cache = new Map(); |
| 34 | return function(arg) { |
| 35 | if (cache.has(arg)) { |
| 36 | console.log("Cache hit:", arg); |
| 37 | return cache.get(arg); |
| 38 | } |
| 39 | console.log("Cache miss:", arg); |
| 40 | const result = fn(arg); |
| 41 | cache.set(arg, result); |
| 42 | return result; |
| 43 | }; |
| 44 | } |
| 45 | |
| 46 | const fib = memoize((n) => n <= 1 ? n : fib(n - 1) + fib(n - 2)); |
| 47 | console.log(fib(40)); // 102334155 — efficiently computed |
best practice
A Set stores unique values of any type. It uses the same SameValueZero equality as Map — each value can appear only once. Sets are ideal for deduplication, membership testing, and as building blocks for mathematical set operations.
| 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 (insertion order) |
| 22 | for (const val of set) { |
| 23 | console.log(val); |
| 24 | } |
| 25 | |
| 26 | set.forEach((val) => console.log(val)); |
| 27 | |
| 28 | // Practical: deduplicate arrays |
| 29 | const duplicates = [1, 2, 2, 3, 4, 4, 5]; |
| 30 | const unique = [...new Set(duplicates)]; // [1, 2, 3, 4, 5] |
| 31 | |
| 32 | // Practical: deduplicate objects by a property |
| 33 | const users = [ |
| 34 | { id: 1, name: "Alice" }, |
| 35 | { id: 2, name: "Bob" }, |
| 36 | { id: 1, name: "Alice" }, // duplicate |
| 37 | ]; |
| 38 | const seen = new Set(); |
| 39 | const uniqueUsers = users.filter((user) => { |
| 40 | if (seen.has(user.id)) return false; |
| 41 | seen.add(user.id); |
| 42 | return true; |
| 43 | }); |
| 44 | |
| 45 | // Practical: check all values unique |
| 46 | function allUnique(arr) { |
| 47 | return new Set(arr).size === arr.length; |
| 48 | } |
| 49 | console.log(allUnique([1, 2, 3])); // true |
| 50 | console.log(allUnique([1, 2, 2])); // false |
info
JavaScript's Set does not have built-in union, intersection, or difference methods (yet — they are coming in a future proposal). However, you can implement them efficiently in a few lines. These operations are essential for data processing, permission systems, and state management.
| 1 | // Set operation helpers |
| 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([...a].filter(x => !b.has(x)).concat([...b].filter(x => !a.has(x)))); |
| 16 | } |
| 17 | |
| 18 | function isSubset(a, b) { |
| 19 | return [...a].every(x => b.has(x)); |
| 20 | } |
| 21 | |
| 22 | function isSuperset(a, b) { |
| 23 | return [...b].every(x => a.has(x)); |
| 24 | } |
| 25 | |
| 26 | // Usage |
| 27 | const A = new Set([1, 2, 3, 4, 5]); |
| 28 | const B = new Set([4, 5, 6, 7, 8]); |
| 29 | |
| 30 | console.log([...union(A, B)]); // [1, 2, 3, 4, 5, 6, 7, 8] |
| 31 | console.log([...intersection(A, B)]); // [4, 5] |
| 32 | console.log([...difference(A, B)]); // [1, 2, 3] |
| 33 | console.log([...difference(B, A)]); // [6, 7, 8] |
| 34 | console.log([...symmetricDifference(A, B)]); // [1, 2, 3, 6, 7, 8] |
| 35 | console.log(isSubset(new Set([1, 2]), A)); // true |
| 36 | console.log(isSuperset(A, new Set([1, 2]))); // true |
| 37 | |
| 38 | // Practical: permission system |
| 39 | const userPermissions = new Set(["read", "write", "delete"]); |
| 40 | const requiredPermissions = new Set(["read", "write"]); |
| 41 | |
| 42 | function hasAllPermissions(user, required) { |
| 43 | return isSuperset(user, required); |
| 44 | } |
| 45 | |
| 46 | console.log(hasAllPermissions(userPermissions, requiredPermissions)); // true |
| 47 | |
| 48 | // Practical: tagging system |
| 49 | function getCommonTags(tagSets) { |
| 50 | return tagSets.reduce((acc, tags) => intersection(acc, tags)); |
| 51 | } |
| 52 | |
| 53 | const tag1 = new Set(["js", "react", "web"]); |
| 54 | const tag2 = new Set(["js", "react", "mobile"]); |
| 55 | const tag3 = new Set(["js", "vue", "web"]); |
| 56 | console.log([...getCommonTags([tag1, tag2, tag3])]); // ["js"] |
pro tip
A WeakMap is like a Map but with a critical difference: 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 important constraints: keys must be objects (not primitives), there is no size property or iteration methods (keys(), values(), entries()), and they cannot be cleared. These constraints exist because the contents could change at any time due to garbage collection — iteration would be unreliable.
| Feature | Map | WeakMap |
|---|---|---|
| Key types | Any | Objects only |
| Iteration | ✓ | ✗ |
| size | ✓ | ✗ |
| clear() | ✓ | ✗ |
| Garbage collection | Prevents GC | Permits GC |
| Memory leak risk | High (retains keys) | Low (releases keys) |
| 1 | // WeakMap basics — object keys only |
| 2 | const weakMap = new WeakMap(); |
| 3 | |
| 4 | let user = { id: 1, name: "Alice" }; |
| 5 | |
| 6 | weakMap.set(user, "session-token-abc123"); |
| 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 | // At this point, the WeakMap entry may be garbage collected |
| 13 | // weakMap.get(user) would now return undefined (user is null) |
| 14 | |
| 15 | // Private data with WeakMap |
| 16 | const _private = new WeakMap(); |
| 17 | |
| 18 | class Person { |
| 19 | constructor(name, ssn) { |
| 20 | // Store private data in WeakMap — not accessible from outside |
| 21 | _private.set(this, { ssn }); |
| 22 | this.name = name; |
| 23 | } |
| 24 | |
| 25 | getSSN() { |
| 26 | return _private.get(this).ssn; |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | const person = new Person("Alice", "123-45-6789"); |
| 31 | console.log(person.name); // "Alice" |
| 32 | console.log(person.ssn); // undefined — not accessible |
| 33 | console.log(person.getSSN()); // "123-45-6789" — accessed via privileged method |
| 34 | |
| 35 | // Preventing prototype pollution: use WeakMap instead of private properties |
| 36 | const metadata = new WeakMap(); |
| 37 | |
| 38 | function setMetadata(obj, data) { |
| 39 | metadata.set(obj, data); |
| 40 | } |
| 41 | |
| 42 | function getMetadata(obj) { |
| 43 | return metadata.get(obj); |
| 44 | } |
| 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 | // When element is removed from DOM, the WeakMap entry |
| 13 | // automatically becomes eligible for garbage collection |
| 14 | // No explicit cleanup needed! |
| 15 | |
| 16 | document.querySelectorAll(".tracked").forEach((el) => { |
| 17 | trackElement(el); |
| 18 | el.addEventListener("click", () => { |
| 19 | const data = nodeData.get(el); |
| 20 | data.clickCount++; |
| 21 | console.log(`Clicked ${data.clickCount} times`); |
| 22 | }); |
| 23 | }); |
| 24 | |
| 25 | // Practical: caching computed results on objects |
| 26 | const cache = new WeakMap(); |
| 27 | |
| 28 | function computeExpensive(obj) { |
| 29 | if (cache.has(obj)) { |
| 30 | console.log("Returning cached result"); |
| 31 | return cache.get(obj); |
| 32 | } |
| 33 | const result = expensiveComputation(obj); |
| 34 | cache.set(obj, result); |
| 35 | return result; |
| 36 | } |
| 37 | |
| 38 | // When obj is no longer used elsewhere, the cache entry |
| 39 | // is automatically cleaned up — no memory leak! |
| 40 | |
| 41 | // Practical: event handler cleanup |
| 42 | const handlerMap = new WeakMap(); |
| 43 | |
| 44 | function addTrackedListener(element, event, handler) { |
| 45 | if (!handlerMap.has(element)) { |
| 46 | handlerMap.set(element, new Map()); |
| 47 | } |
| 48 | handlerMap.get(element).set(event, handler); |
| 49 | element.addEventListener(event, handler); |
| 50 | } |
| 51 | |
| 52 | function removeAllListeners(element) { |
| 53 | const handlers = handlerMap.get(element); |
| 54 | if (handlers) { |
| 55 | for (const [event, handler] of handlers) { |
| 56 | element.removeEventListener(event, handler); |
| 57 | } |
| 58 | handlerMap.delete(element); |
| 59 | } |
| 60 | } |
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. Like WeakMap, WeakSet has no iteration, no size, and only accepts object values. It is useful for tagging or marking objects without preventing their 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 | |
| 12 | page1 = null; // page1 removed from visited automatically when GC'd |
| 13 | // At this point visited.has(page1) would return false |
| 14 | |
| 15 | // Practical: prevent double-processing |
| 16 | const processed = new WeakSet(); |
| 17 | |
| 18 | function processItem(item) { |
| 19 | if (processed.has(item)) { |
| 20 | console.log("Already processed, skipping:", item.id); |
| 21 | return; |
| 22 | } |
| 23 | // ... process item ... |
| 24 | processed.add(item); |
| 25 | console.log("Processed:", item.id); |
| 26 | } |
| 27 | |
| 28 | const items = [ |
| 29 | { id: 1 }, { id: 2 }, { id: 1 } // duplicate |
| 30 | ]; |
| 31 | |
| 32 | items.forEach(processItem); |
| 33 | // Processed: 1 |
| 34 | // Processed: 2 |
| 35 | // Already processed, skipping: 1 |
| 36 | |
| 37 | // Practical: marking objects for special handling |
| 38 | const isAuthorized = new WeakSet(); |
| 39 | const isAdmin = new WeakSet(); |
| 40 | |
| 41 | function grantAccess(user) { |
| 42 | isAuthorized.add(user); |
| 43 | } |
| 44 | |
| 45 | function grantAdmin(user) { |
| 46 | isAuthorized.add(user); |
| 47 | isAdmin.add(user); |
| 48 | } |
| 49 | |
| 50 | function checkAccess(user) { |
| 51 | return isAuthorized.has(user); |
| 52 | } |
| 53 | |
| 54 | function checkAdmin(user) { |
| 55 | return isAdmin.has(user); |
| 56 | } |
| 57 | |
| 58 | // When user objects are no longer referenced anywhere, |
| 59 | // their membership in these sets is automatically cleaned up |
info
WeakRef (ES2021) is a lower-level primitive that holds a weak reference to an object. Unlike WeakMap/WeakSet which only store metadata, a WeakRef lets you hold a reference that does not prevent GC. You access the object via .deref(), which returns the object if it is still alive or undefined if it has been collected.
WeakRef is useful for caches, pools, and registries where retaining objects indefinitely would waste memory. However, use it carefully — garbage collection timing is non-deterministic and engine-specific. A value may stay alive much longer than expected, or be collected sooner.
| 1 | // WeakRef basics |
| 2 | let target = { data: "important" }; |
| 3 | const ref = new WeakRef(target); |
| 4 | |
| 5 | // Access the value (may be collected) |
| 6 | console.log(ref.deref()?.data); // "important" |
| 7 | |
| 8 | // Remove all strong references |
| 9 | target = null; |
| 10 | |
| 11 | // At some later point (non-deterministic): |
| 12 | // console.log(ref.deref()); // Could be the object or undefined |
| 13 | |
| 14 | // Practical: caching without preventing GC |
| 15 | function createWeakCache() { |
| 16 | const cache = new Map(); // Consider using Map of WeakRefs |
| 17 | |
| 18 | return { |
| 19 | get(key) { |
| 20 | const ref = cache.get(key); |
| 21 | if (ref) return ref.deref(); |
| 22 | return undefined; |
| 23 | }, |
| 24 | set(key, value) { |
| 25 | cache.set(key, new WeakRef(value)); |
| 26 | }, |
| 27 | has(key) { |
| 28 | return cache.has(key) && cache.get(key).deref() !== undefined; |
| 29 | } |
| 30 | }; |
| 31 | } |
| 32 | |
| 33 | // Practical: image cache that releases memory under pressure |
| 34 | const imageCache = new Map(); |
| 35 | |
| 36 | function fetchImage(url) { |
| 37 | const existing = imageCache.get(url); |
| 38 | if (existing) { |
| 39 | const img = existing.deref(); |
| 40 | if (img) return Promise.resolve(img); |
| 41 | imageCache.delete(url); |
| 42 | } |
| 43 | return loadImage(url).then((img) => { |
| 44 | imageCache.set(url, new WeakRef(img)); |
| 45 | return img; |
| 46 | }); |
| 47 | } |
warning
FinalizationRegistry (ES2021) lets you register a callback that is invoked when an object is garbage collected. This is useful for cleaning up external resources (file handles, network connections, native memory) when the JavaScript object is no longer reachable.
Like WeakRef, FinalizationRegistry is non-deterministic. The callback may never be called during the lifetime of your program, or it may be called long after the object is unreachable. Never rely on it for correctness — use it as a safety net, not as a primary cleanup mechanism.
| 1 | // FinalizationRegistry basics |
| 2 | const registry = new FinalizationRegistry((heldValue) => { |
| 3 | console.log("Object collected, held value:", heldValue); |
| 4 | // Perform cleanup based on heldValue |
| 5 | }); |
| 6 | |
| 7 | function createResource() { |
| 8 | const resource = { handle: "file-123" }; |
| 9 | registry.register(resource, "cleanup-file-123"); |
| 10 | return resource; |
| 11 | } |
| 12 | |
| 13 | let res = createResource(); |
| 14 | res = null; // When GC runs, the callback will fire with "cleanup-file-123" |
| 15 | |
| 16 | // Practical: cleaning up native resources |
| 17 | class NativeFileHandle { |
| 18 | constructor(path) { |
| 19 | this.path = path; |
| 20 | this.fd = openFileDescriptor(path); // native resource |
| 21 | |
| 22 | // Register cleanup — if the user forgets to close() |
| 23 | this._cleanup = new FinalizationRegistry((fd) => { |
| 24 | console.log("Finalizer: closing file descriptor", fd); |
| 25 | closeFileDescriptor(fd); |
| 26 | }); |
| 27 | this._cleanup.register(this, this.fd); |
| 28 | } |
| 29 | |
| 30 | read() { |
| 31 | // Use this.fd |
| 32 | } |
| 33 | |
| 34 | close() { |
| 35 | if (this.fd !== null) { |
| 36 | closeFileDescriptor(this.fd); |
| 37 | this.fd = null; |
| 38 | // Unregister to prevent double-close |
| 39 | this._cleanup.unregister(this); |
| 40 | } |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // Usage — close() is the primary cleanup, FinalizationRegistry is backup |
| 45 | function processFile(path) { |
| 46 | const file = new NativeFileHandle(path); |
| 47 | // If processFile throws and file is dropped, FD is still cleaned up eventually |
| 48 | const data = file.read(); |
| 49 | file.close(); |
| 50 | return data; |
| 51 | } |
danger
Choosing between Map and Object is a common design decision. Both store key-value pairs, but they have different characteristics. Here is a comprehensive comparison to guide your choice.
| Criterion | Map | Object |
|---|---|---|
| Key types | Any (objects, functions, NaN) | Strings and Symbols only |
| Order | Insertion order guaranteed | Integer keys first, then insertion order for strings |
| size | O(1) via .size | O(n) via Object.keys().length |
| Iteration | Directly iterable (for...of) | Via Object.keys/values/entries |
| Performance (set/get) | Excellent — specialized | Excellent — highly optimized for V8 |
| JSON | No direct support | Native (JSON.stringify/parse) |
| Prototype | No prototype chain | Has prototype (inherited keys) |
| Default keys | None | Inherits from prototype (toString, etc.) |
| Spread | [...map] gives entries | {...obj} copies properties |
| Destructuring literal | N/A | const { key } = obj |
| 1 | // When to use Object: |
| 2 | // 1. Fixed string keys known at author time |
| 3 | const config = { host: "localhost", port: 3000 }; |
| 4 | |
| 5 | // 2. JSON serialization needed |
| 6 | const json = JSON.stringify(config); |
| 7 | const parsed = JSON.parse(json); |
| 8 | |
| 9 | // 3. Destructuring pattern |
| 10 | const { host, port } = config; |
| 11 | |
| 12 | // 4. Prototype-based property lookup |
| 13 | const event = new Event("click"); |
| 14 | console.log(event.type); // "click" — inherited from prototype |
| 15 | |
| 16 | // When to use Map: |
| 17 | // 1. Dynamic keys, especially non-string |
| 18 | const userRoles = new Map(); |
| 19 | userRoles.set(alice, "admin"); |
| 20 | userRoles.set(bob, "user"); |
| 21 | |
| 22 | // 2. Frequent addition/removal |
| 23 | const cache = new Map(); |
| 24 | cache.set("key", value); |
| 25 | cache.delete("key"); |
| 26 | |
| 27 | // 3. Need reliable iteration or size |
| 28 | console.log(cache.size); |
| 29 | for (const [k, v] of cache) { /* ... */ } |
| 30 | |
| 31 | // 4. Map from another Map |
| 32 | const copy = new Map(original); |
| 33 | |
| 34 | // 5. Keys are not known upfront (user data, etc.) |
| 35 | const metrics = new Map(); |
| 36 | users.forEach((user) => { |
| 37 | metrics.set(user.id, computeMetrics(user)); |
| 38 | }); |
| 39 | |
| 40 | // Performance: both are fast, but V8 can optimize |
| 41 | // hash maps better when the shape is predictable. |
| 42 | // For most cases, choose by API needs, not performance. |
best practice
Understanding the relative performance characteristics of collection types helps make informed decisions. The benchmarks below represent typical operations on collections of various sizes. Results are engine-specific (V8 in Chrome/Node.js shown here).
| Operation | Small (10 items) | Medium (10K items) | Large (1M items) | Winner |
|---|---|---|---|---|
| Map.set/get | ~0.01ms | ~0.02ms | ~0.05ms | Map |
| Object set/get | ~0.01ms | ~0.03ms | ~0.15ms | Map (large sets) |
| Set.add/has | ~0.01ms | ~0.02ms | ~0.05ms | Set |
| Array.includes | ~0.01ms | ~1.2ms | ~120ms | Set (large) |
| Map iteration | ~0.005ms | ~0.3ms | ~4ms | Map |
| Object.keys iteration | ~0.005ms | ~0.4ms | ~5ms | Comparable |
| Map delete | ~0.01ms | ~0.02ms | ~0.05ms | Map |
| Object delete | ~0.02ms | ~0.06ms | ~0.3ms | Map (larger sets) |
| 1 | // Performance test methodology (rough benchmarks) |
| 2 | // These are not exact — results vary by engine, runtime, and data shape |
| 3 | |
| 4 | function benchmark(label, iterations, fn) { |
| 5 | const start = performance.now(); |
| 6 | for (let i = 0; i < iterations; i++) fn(i); |
| 7 | const elapsed = performance.now() - start; |
| 8 | console.log(`${label}: ${elapsed.toFixed(2)}ms (${iterations} iterations)`); |
| 9 | } |
| 10 | |
| 11 | // Sample benchmark: Map.get vs Object access |
| 12 | const size = 100000; |
| 13 | const map = new Map(); |
| 14 | const obj = {}; |
| 15 | |
| 16 | for (let i = 0; i < size; i++) { |
| 17 | map.set(i, i); |
| 18 | obj[i] = i; |
| 19 | } |
| 20 | |
| 21 | // Warmup |
| 22 | for (let i = 0; i < 1000; i++) { |
| 23 | map.get(i); obj[i]; |
| 24 | } |
| 25 | |
| 26 | // Benchmark |
| 27 | benchmark("Map get", 100000, (i) => map.get(i)); |
| 28 | benchmark("Object access", 100000, (i) => obj[i]); |
| 29 | |
| 30 | // Set vs Array.includes |
| 31 | const arr = Array.from({ length: 10000 }, (_, i) => i); |
| 32 | const set = new Set(arr); |
| 33 | const target = 9999; |
| 34 | |
| 35 | benchmark("Array.includes", 10000, () => arr.includes(target)); |
| 36 | benchmark("Set.has", 10000, () => set.has(target)); |
| 37 | // Set.has is dramatically faster for large collections (~O(1) vs ~O(n)) |
| 38 | |
| 39 | // Key takeaway: for lookups on >1000 items, use Set/Map over Array/Object |
pro tip
| 1 | // Quick reference |
| 2 | |
| 3 | // Create |
| 4 | const map = new Map([["key", "value"]]); |
| 5 | const set = new Set([1, 2, 3]); |
| 6 | const wm = new WeakMap(); |
| 7 | const ws = new WeakSet(); |
| 8 | |
| 9 | // Map operations |
| 10 | map.set(key, val); // Add/update |
| 11 | map.get(key); // Read |
| 12 | map.has(key); // Check |
| 13 | map.delete(key); // Remove |
| 14 | map.size; // Count |
| 15 | map.clear(); // Empty |
| 16 | |
| 17 | // Set operations |
| 18 | set.add(val); // Add |
| 19 | set.has(val); // Check |
| 20 | set.delete(val); // Remove |
| 21 | set.size; // Count |
| 22 | set.clear(); // Empty |
| 23 | |
| 24 | // WeakMap/WeakSet |
| 25 | wm.set(obj, val); // Add (obj key only) |
| 26 | wm.get(obj); // Read |
| 27 | wm.has(obj); // Check |
| 28 | wm.delete(obj); // Remove |
| 29 | |
| 30 | // Conversions |
| 31 | const asArray = [...map]; // Map → [key,value][] |
| 32 | const fromArray = new Map(asArray); // [key,value][] → Map |
| 33 | const asObject = Object.fromEntries(map); // Map → Object |
| 34 | const asMap = new Map(Object.entries(obj)); // Object → Map |
| 35 | |
| 36 | // Deduplicate |
| 37 | const unique = [...new Set(array)]; |
| 38 | |
| 39 | // Fast membership |
| 40 | const lookup = new Set(items); |
| 41 | if (lookup.has(target)) { /* O(1) */ } |
warning