JS Sets & Maps
Master modern high-performance keyed and unique collections: Map, Set, WeakMap, and WeakSet. Discover \(O(1)\) lookup benchmarks, iteration protocols, modern ES2024 Set operations, and memory lifecycle mechanics.
Table of Contents
14.1 The Limitations of Objects & Arrays (Why We Need Map & Set)
Before ECMAScript 2015 (ES6), JavaScript developers used plain Objects for key-value dictionaries and Arrays for lists of values. While ubiquitous, these legacy data structures possess severe architectural flaws when applied to large-scale, high-performance data processing:
Plain Object Limitations
- String/Symbol Key Coercion: Keys are forced to strings. If you use an object as a key,
obj[{}] = 1converts the key to"[object Object]", overwriting previous object keys! - Prototype Collisions & Pollution: Objects inherit from
Object.prototype. Default keys liketoString,constructor, and__proto__can be hijacked or cause unexpected collisions. - No Fast Size Check: Calculating entry count requires
Object.keys(obj).length, which traverses all keys in \(O(N)\) time. - Engine Deoptimization: Rapidly adding and deleting dynamic keys forces V8 to invalidate Hidden Classes, degrading hash lookup speeds.
Array Uniqueness Limitations
- \(O(N)\) Membership Search: Checking whether a value exists using
arr.includes(x)orarr.indexOf(x)requires a linear scan through the entire array. - \(O(N^2)\) Deduplication: Filtering duplicates via
arr.filter((v, i) => arr.indexOf(v) === i)scales quadratically, freezing the browser on arrays with 50,000+ elements. - Expensive In-Place Removal: Deleting an item with
arr.splice(index, 1)shifts every subsequent element in memory, imposing an \(O(N)\) copy overhead.
The Object Key Overwrite Bug vs Map
Notice how plain objects coerce all objects into "[object Object]", while Map retains exact object identity.
// 1. The Plain Object Trap:
const userA = { id: 101, name: "Alice" };
const userB = { id: 102, name: "Bob" };
const scores = {};
scores[userA] = 95;
scores[userB] = 88; // OVERWRITES userA because both become "[object Object]"!
console.log(scores);
// Output: { "[object Object]": 88 }
// 2. The Map Solution:
const mapScores = new Map();
mapScores.set(userA, 95);
mapScores.set(userB, 88);
console.log(mapScores.get(userA)); // 95 (Preserved!)
console.log(mapScores.get(userB)); // 88 (Preserved!)
console.log(mapScores.size); // 2 (Instant O(1) size check)
Try it Yourself »
14.2 The JavaScript Map Collection: Complete API Guide
A Map is an ordered collection of key-value pairs where any value (both primitive values and objects) can be used as either a key or a value. Unlike plain objects, a Map remembers the original insertion order of the keys.
| Method / Property | Return Type | Time Complexity | Description |
|---|---|---|---|
new Map([iterable]) |
Map |
\(O(N)\) | Creates a new Map, optionally pre-populated from an array of [key, value] pairs. |
map.set(key, value) |
Map |
\(O(1)\) amortized | Stores the key-value pair. Returns the map itself, enabling fluent method chaining. |
map.get(key) |
any | undefined |
\(O(1)\) amortized | Retrieves the value associated with the key, or undefined if not present. |
map.has(key) |
boolean |
\(O(1)\) amortized | Returns true if the key exists in the map; false otherwise. |
map.delete(key) |
boolean |
\(O(1)\) amortized | Removes the key-value pair. Returns true if removed, false if not found. |
map.clear() |
undefined |
\(O(1)\) | Removes all key-value pairs from the map. |
map.size |
number |
\(O(1)\) | Property returning the current number of elements (NOT a method call). |
Map Instantiation, CRUD & Chaining
// Pre-populating a Map via 2D Array of [key, value] pairs:
const productInventory = new Map([
["SKU-100", { name: "Mechanical Keyboard", qty: 24 }],
["SKU-200", { name: "Gaming Mouse", qty: 45 }]
]);
// Fluent Chaining with .set()
productInventory
.set("SKU-300", { name: "USB-C Hub", qty: 12 })
.set("SKU-400", { name: "4K Webcam", qty: 8 });
console.log(productInventory.size); // 4
// Check existence & retrieve
if (productInventory.has("SKU-200")) {
const item = productInventory.get("SKU-200");
console.log(`In stock: ${item.qty} units of ${item.name}`);
}
// Delete an item
const removed = productInventory.delete("SKU-100"); // returns true
console.log("Remaining items:", productInventory.size); // 3
Try it Yourself »
Map Key Equality: The SameValueZero Algorithm
JavaScript Map keys are compared using the SameValueZero specification. This means:
• NaN is considered equal to NaN (unlike === where NaN !== NaN).
• +0 and -0 are considered identical.
• Objects, functions, and arrays are compared by memory address reference, not by structural value. Two distinct objects {} === {} will always produce two different entries in a Map!
14.3 Map Iteration Protocols & Data Transformations
A Map implements the Iterable protocol. This means you can loop over its keys, values, or entries with for...of, spread it into arrays, and convert back and forth with plain objects using modern ECMAScript utilities:
Returns an Iterator of keys in their original insertion sequence.
Returns an Iterator of mapped values in their original insertion sequence.
Returns an Iterator of [key, value] pairs (default iterator for for...of).
Iterating & Converting Maps
const userRoles = new Map([
["alice@corp.com", "Admin"],
["bob@corp.com", "Editor"],
["charlie@corp.com", "Viewer"]
]);
// 1. Destructuring in for...of:
for (const [email, role] of userRoles) {
console.log(`${email} holds access: ${role}`);
}
// 2. map.forEach (Note argument order: value first, key second!):
userRoles.forEach((role, email) => {
console.log(`Sending ping to ${email} (${role})`);
});
// 3. Converting Map <-> Object:
const plainObj = Object.fromEntries(userRoles);
console.log(plainObj); // { "alice@corp.com": "Admin", ... }
const backToMap = new Map(Object.entries(plainObj));
// 4. JSON Serialization Strategy:
// JSON.stringify(map) produces "{}" because Maps lack own string properties!
// Always serialize as an array of entries:
const jsonPayload = JSON.stringify([...userRoles]);
console.log(jsonPayload); // '[["alice@corp.com","Admin"],...]'
Try it Yourself »
14.4 Map vs Object Performance Benchmark & Decision Matrix
When should you choose a Map over a plain JavaScript Object? Modern JavaScript engines (V8, JavaScriptCore, SpiderMonkey) optimize plain objects for static record structures using Hidden Classes (Shapes) and Inline Caches. However, when keys are added, retrieved, and deleted frequently, Map outperforms plain objects by orders of magnitude.
| Feature | Plain JavaScript Object {} |
JavaScript Map |
|---|---|---|
| Key Data Types | Strings or Symbols only (coerces everything else). | Any data type (objects, functions, primitives, NaN). |
| Key Ordering | Complex (integer keys sort ascending first, then insertion). | Guaranteed insertion order across all key types. |
| Size Calculation | \(O(N)\) via Object.keys(obj).length. |
\(O(1)\) instant property via map.size. |
| Security & Collisions | Contains prototype keys (toString, __proto__). Vulnerable to pollution. |
Completely clean. No default keys; impervious to prototype pollution. |
| High Churn Performance | Poor. Frequent additions/deletions invalidate V8 Shapes. | Optimized hash bucket table built specifically for continuous churn. |
| JSON Serialization | Native via JSON.stringify(obj). |
Requires converting to entries: JSON.stringify([...map]). |
Live 100,000 Key Churn Benchmark Demo
Run this benchmark in the playground to compare 100,000 rapid key additions and deletions.
const ITERATIONS = 100_000;
// 1. Plain Object Benchmark
console.time("Object Churn");
const obj = {};
for (let i = 0; i < ITERATIONS; i++) {
obj["key_" + i] = i;
}
for (let i = 0; i < ITERATIONS; i++) {
delete obj["key_" + i];
}
console.timeEnd("Object Churn");
// 2. Map Benchmark
console.time("Map Churn");
const map = new Map();
for (let i = 0; i < ITERATIONS; i++) {
map.set("key_" + i, i);
}
for (let i = 0; i < ITERATIONS; i++) {
map.delete("key_" + i);
}
console.timeEnd("Map Churn");
Try it Yourself »
14.5 The JavaScript Set Collection: Unique Value Stores
A Set is a collection of unique values. Any value can occur only once within a Set. Attempting to add a duplicate value has no effect. Like Map, values are iterated in insertion order.
| Method / Property | Return Type | Time Complexity | Description |
|---|---|---|---|
new Set([iterable]) |
Set |
\(O(N)\) | Initializes a Set, automatically stripping duplicate elements from the iterable. |
set.add(value) |
Set |
\(O(1)\) amortized | Appends value if not already present. Returns the Set instance for chaining. |
set.has(value) |
boolean |
\(O(1)\) amortized | Tests if value exists in the Set. Massive \(O(1)\) upgrade over arr.includes() \(O(N)\). |
set.delete(value) |
boolean |
\(O(1)\) amortized | Deletes the value. Returns true if found and removed; false otherwise. |
set.clear() |
undefined |
\(O(1)\) | Empties all elements from the Set. |
set.size |
number |
\(O(1)\) | Current count of unique elements in the Set. |
Set Deduplication & Instant O(1) Membership
// Instant Array Deduplication using Spread Syntax:
const rawTags = ["react", "javascript", "css", "javascript", "react", "html"];
const uniqueTags = [...new Set(rawTags)];
console.log(uniqueTags); // ["react", "javascript", "css", "html"]
// Set of Active User IDs (High-Throughput Membership Testing):
const onlineUsers = new Set([101, 102, 103]);
// Chained additions
onlineUsers.add(104).add(105).add(101); // 101 ignored (duplicate)
console.log(onlineUsers.size); // 5
// O(1) Instant Lookup vs O(N) Array Scan:
if (onlineUsers.has(103)) {
console.log("User 103 is currently active in chat room!");
}
onlineUsers.delete(102);
console.log("Online count:", onlineUsers.size); // 4
Try it Yourself »
14.6 Practical Set Operations & Modern ES2024 Set Theory
Mathematical Set Theory defines operations like Union, Intersection, and Difference. In **ECMAScript 2024**, JavaScript introduced native Set methods that eliminate manual loop filtering. Let's explore both the new standard and universal fallback implementations:
| Operation | Math Notation | ES2024 Native Method | Universal JavaScript Fallback |
|---|---|---|---|
| Union | \(A \cup B\) | A.union(B) | new Set([...A, ...B]) |
| Intersection | \(A \cap B\) | A.intersection(B) | new Set([...A].filter(x => B.has(x))) |
| Difference | \(A \setminus B\) | A.difference(B) | new Set([...A].filter(x => !B.has(x))) |
| Symmetric Difference | \(A \Delta B\) | A.symmetricDifference(B) | new Set([...A.difference(B), ...B.difference(A)]) |
| Is Subset Of | \(A \subseteq B\) | A.isSubsetOf(B) | [...A].every(x => B.has(x)) |
| Is Disjoint From | \(A \cap B = \emptyset\) | A.isDisjointFrom(B) | [...A].every(x => !B.has(x)) |
Executing Set Operations (ES2024 with Fallback Polyfill)
const devA_Skills = new Set(["JavaScript", "TypeScript", "React", "Node"]);
const devB_Skills = new Set(["Python", "React", "Docker", "Node"]);
// 1. Intersection (Skills shared by both developers):
const sharedSkills = devA_Skills.intersection
? devA_Skills.intersection(devB_Skills)
: new Set([...devA_Skills].filter(skill => devB_Skills.has(skill)));
console.log("Shared:", [...sharedSkills]);
// ["React", "Node"]
// 2. Difference (Skills Dev A has that Dev B does NOT have):
const devA_Only = devA_Skills.difference
? devA_Skills.difference(devB_Skills)
: new Set([...devA_Skills].filter(skill => !devB_Skills.has(skill)));
console.log("Dev A Exclusive:", [...devA_Only]);
// ["JavaScript", "TypeScript"]
// 3. Union (Entire combined tech stack):
const techStack = devA_Skills.union
? devA_Skills.union(devB_Skills)
: new Set([...devA_Skills, ...devB_Skills]);
console.log("Total Stack:", [...techStack]);
// ["JavaScript", "TypeScript", "React", "Node", "Python", "Docker"]
Try it Yourself »
14.7 WeakMap: Ephemeral Memory & Garbage Collection Mechanics
In a standard Map, storing an object as a key creates a strong reference. Even if your application deletes all other references to that object, the Map keeps the object alive in memory, preventing the JavaScript Garbage Collector (GC) from freeing it — causing insidious memory leaks in Single Page Applications (SPAs).
A WeakMap resolves this dilemma: it holds weak references to its keys. If no other strong references to a key object exist in the program, the engine automatically reclaims both the key object and its associated value during the next GC cycle!
WeakMap Constraints
- Keys Must Be Objects: Primitives (strings, numbers) cannot be keys because primitives are immutable values without garbage-collected memory addresses.
- Not Enumerable: Has NO
.size,.keys(),.values(),.entries(), or.forEach(). Iterating would be non-deterministic because the engine can trigger garbage collection at any moment. - Only 4 Methods:
get(key),set(key, val),has(key), anddelete(key).
Primary Use Cases
- DOM Element Metadata: Associating component state, event counters, or caches with DOM elements without leaking memory when the element is removed from the DOM.
- Private Class Data: Storing private instance variables keyed by
this(popular before native#privatefields). - Memoization of Object Arguments: Caching the result of expensive calculations on complex objects without holding them indefinitely in memory.
Leak-Free DOM Metadata Caching with WeakMap
// Storing analytics/metadata on DOM nodes without memory leaks:
const elementClickTracker = new WeakMap();
function registerButtonClick(buttonElement) {
// If button not tracked, start with 0
const currentClicks = elementClickTracker.get(buttonElement) || 0;
elementClickTracker.set(buttonElement, currentClicks + 1);
console.log(`Button clicked ${currentClicks + 1} times!`);
}
// When a button is created:
let btn = document.createElement("button");
registerButtonClick(btn); // 1
registerButtonClick(btn); // 2
// Later, the button is removed from the UI and dereferenced:
btn.remove();
btn = null;
// GC AUTOMATION:
// In a Map, btn would remain locked in memory forever.
// In WeakMap, the GC automatically reclaims the button AND click count!
Try it Yourself »
14.8 WeakSet: Garbage-Collected Object Tagging
Similar to WeakMap, a WeakSet is a collection of objects only where each object is held by a weak reference. If an object stored in a WeakSet has no other active references in your program, the garbage collector disposes of it automatically.
A WeakSet supports only three methods: set.add(obj), set.has(obj), and set.delete(obj). It is non-enumerable (no .size and no iteration).
Detecting Circular References via WeakSet
Notice how WeakSet tags visited objects during recursive traversal without holding memory leaks.
function isCircular(obj, seen = new WeakSet()) {
if (typeof obj !== "object" || obj === null) return false;
// If already tagged in WeakSet, we found a circular reference loop!
if (seen.has(obj)) return true;
seen.add(obj);
for (const key of Object.keys(obj)) {
if (isCircular(obj[key], seen)) return true;
}
return false;
}
// Test Graph 1: Normal Tree
const tree = { name: "Root", child: { name: "Branch" } };
console.log("Tree is circular:", isCircular(tree)); // false
// Test Graph 2: Circular Graph
const nodeA = { id: "A" };
const nodeB = { id: "B" };
nodeA.next = nodeB;
nodeB.next = nodeA; // Loop!
console.log("Graph is circular:", isCircular(nodeA)); // true
Try it Yourself »
14.9 Memory & Lookup Visual Architecture
These high-resolution interactive architecture diagrams reveal how JavaScript engines organize key-value lookups under the hood and how strong vs weak references dictate memory garbage collection.
14.10 Interactive Mini-Labs (Live in Chapter Body)
Experiment with live interactive laboratory widgets running directly in your browser: test multi-type Map caches, visual Set operations, and real-time garbage collection simulations.
Mini-Lab 1: Multi-Type Map Cache Inspector
Live JavaScript Map
Select a diverse key type and click Add Entry to observe how Map handles objects, functions, numbers, and strings as first-class keys without coercion:
Mini-Lab 2: Visual Set Operations Workbench
ES2024 & Fallback EngineToggle tags in Set A and Set B, then select an operation to visualize the mathematical result:
Mini-Lab 3: Garbage Collection & Memory Leak Simulator
Memory SimulatorSimulate creating 4 temporary dynamic widgets and caching them in either a Standard Map (Strong Reference) or a WeakMap (Weak Reference). Remove the widgets from the DOM and run the Garbage Collector to observe the outcome:
14.11 Hands-on Challenge: Build a High-Performance LRU Cache
A **Least Recently Used (LRU) Cache** is a ubiquitous data structure used in browsers, databases, and CDNs. It stores up to \(N\) items. When capacity is exceeded, it automatically evicts the key that has not been accessed for the longest time.
Challenge Specification
Implement a class LRUCache using JavaScript's native Map that satisfies the following requirements:
constructor(capacity): Initializes the cache with a positive integer capacity.get(key): Returns the value of the key if it exists, otherwise-1. If accessed, the key must be marked as most recently used. Must run in \(O(1)\) time.put(key, value): Updates the key if it exists, or inserts it. If inserting exceeds capacity, evict the least recently used key before inserting. Must run in \(O(1)\) time.- Architectural Hint: Leverage
Map's guaranteed insertion order! When an entry is read or updated, delete it and re-set it withmap.delete(key); map.set(key, val)to move it to the most recently used end of the map. The oldest entry is alwaysmap.keys().next().value!
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
// Refresh position to "most recently used" (tail of Map)
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
put(key, value) {
// If key already exists, delete so re-insertion moves it to tail
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// Evict least recently used (first key in iterator)
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}
// ── Test Verification ──
const lru = new LRUCache(2);
lru.put("A", 1);
lru.put("B", 2);
console.log(lru.get("A")); // returns 1 (A is now most recently used)
lru.put("C", 3); // Capacity reached! Evicts "B" (least recently used)
console.log(lru.get("B")); // returns -1 (Evicted!)
console.log(lru.get("C")); // returns 3
console.log(lru.get("A")); // returns 1
Try it Yourself »
Chapter 14 — Key Takeaways
Mapallows any data type as keys (functions, objects, NaN).Mapmaintains guaranteed insertion order;map.sizeis \(O(1)\).Mapis optimized for high-frequency additions/deletions.Setguarantees uniqueness;[...new Set(arr)]deduplicates in \(O(N)\).set.has()performs in \(O(1)\) vsarr.includes()in \(O(N)\).
- ES2024 Set Methods:
union,intersection,difference,symmetricDifference. WeakMapkeys must be objects; held by weak references.WeakMap&WeakSetprevent memory leaks; automatically swept by GC.- Weak collections are not enumerable (no
.size, no iteration). - Module 2 Complete: Ready for Module 3 (Errors, Debugging & Quality)!