Chapter 1 of ?
js 25 min read

JavaScript Mastery — Chapter 14: JS Sets & Maps

Module 2: Objects, Arrays & Collections Chapter 14

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.

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[{}] = 1 converts the key to "[object Object]", overwriting previous object keys!
  • Prototype Collisions & Pollution: Objects inherit from Object.prototype. Default keys like toString, 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) or arr.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:

map.keys()

Returns an Iterator of keys in their original insertion sequence.

map.values()

Returns an Iterator of mapped values in their original insertion sequence.

map.entries()

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), and delete(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 #private fields).
  • 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.

Diagram 1: Keyed Collection Lookup Architecture (Map vs Object vs Array)
Swipe left/right to view full diagram — or tap Fullscreen for best experience
JavaScript Map Hash Table — O(1) Lookup [ Hash Bucket Array ] key → value (any type) Insertion-Order Node A key → value (any type) Insertion-Order Node B ✓ Any-type key (object, fn, symbol) ✓ .size is O(1) ✓ Stable insertion order ✓ Instant Hash Indexing + Ordered Plain Object V8 Hidden Class — Fast if Static [ String Key Dictionary ] Hidden Class (Shape ID) Fast if shape is stable Object.prototype Chain Prototype lookup fallback ⚠ Keys coerced to string ⚠ Prototype key pollution risk ⚠ Deopts on dynamic key churn ⚠ Shape-dependent performance Array Search Linear Scan — O(N) Lookup Contiguous Memory Slots [0] [1] [2] [N] → scan starts at [0], walks to [N] arr.includes(x) — full scan ✗ O(N) membership test ✗ O(N²) deduplication ✗ O(N) splice shift on delete Use Set for O(1) membership instead Scales poorly on large datasets ✗ O(N) scan & O(N) splice shift
Diagram 2: Garbage Collection Lifecycle (Strong Map vs WeakMap)
Swipe left/right to view full diagram — or tap Fullscreen for best experience
Standard Map — Strong Reference ⚠ Memory Leak Risk Stack Root user = null Heap: { id: 101 } root reference removed Map Instance Holds STRONG key pointer Locks Object! Even with root = null, Map keeps object alive. GC sees Map → Heap strong edge → skips collection. ✗ GC cannot collect — Object retained indefinitely WeakMap — Weak Reference ✓ Automatic GC Sweep Stack Root user = null Heap: Discarded Garbage Collected ✓ WeakMap Instance Holds WEAK key pointer Weak Pointer With root = null, no strong refs remain. GC sees only weak edge → collects object + entry. ✓ GC sweeps object & entry — zero memory leak

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:

ACTIVE MAP ENTRIES Current size: 0
Map is currently empty. Add an entry above!
LIVE CODE REPRESENTATION:
const liveMap = new Map();
Mini-Lab 2: Visual Set Operations Workbench
ES2024 & Fallback Engine

Toggle tags in Set A and Set B, then select an operation to visualize the mathematical result:

Set A Tags (Click to toggle):
Set B Tags (Click to toggle):
RESULT SET: A ∪ B
setA.union(setB);
Mini-Lab 3: Garbage Collection & Memory Leak Simulator
Memory Simulator

Simulate 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:

System initialized: 4 widgets attached to DOM and cached.

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:

  1. constructor(capacity): Initializes the cache with a positive integer capacity.
  2. 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.
  3. 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.
  4. Architectural Hint: Leverage Map's guaranteed insertion order! When an entry is read or updated, delete it and re-set it with map.delete(key); map.set(key, val) to move it to the most recently used end of the map. The oldest entry is always map.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

  • Map allows any data type as keys (functions, objects, NaN).
  • Map maintains guaranteed insertion order; map.size is \(O(1)\).
  • Map is optimized for high-frequency additions/deletions.
  • Set guarantees uniqueness; [...new Set(arr)] deduplicates in \(O(N)\).
  • set.has() performs in \(O(1)\) vs arr.includes() in \(O(N)\).
  • ES2024 Set Methods: union, intersection, difference, symmetricDifference.
  • WeakMap keys must be objects; held by weak references.
  • WeakMap & WeakSet prevent 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)!
Done with this chapter?
Mark it complete to track your progress and unlock your certificate.
Next Up

Learner Reviews

Write a Review
Share your experience to help other learners.
Your Rating *