MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

JavaScript Map, Set, WeakMap, and WeakSet

Master JS collections: Map, Set, WeakMap, WeakSet, and when to use each.

JavaScriptMapSetData Structures

By MinhVo

Introduction

JavaScript provides four powerful collection types beyond plain objects and arrays: Map, Set, WeakMap, and WeakSet. Each serves distinct purposes and offers unique performance characteristics for specific use cases. Understanding these collections deeply — their internal mechanics, performance tradeoffs, and ideal use cases — separates competent JavaScript developers from experts.

This guide explores when to use each collection, their internal mechanics, performance benchmarks, and practical patterns for real-world applications. You will learn not just the API surface but the underlying implementation details that explain why certain collections outperform others in specific scenarios.

Data Structures and Collections in JavaScript

Understanding JavaScript Collections: Core Concepts

Map vs Object: The Fundamental Difference

The most common question developers face is when to use a Map versus a plain object. The answer lies in understanding their internal implementations and the constraints each imposes.

Object keys are always coerced to strings or symbols. This implicit coercion causes subtle bugs:

// Object keys are always strings/symbols
const obj = {};
obj[1] = 'one';
obj['1'] = 'ONE'; // Overwrites the number key!
console.log(obj); // { '1': 'ONE' }
 
// Demonstration of the coercion problem
const cache = {};
cache[{ id: 1 }] = 'value';
console.log(cache[{ id: 1 }]); // undefined — different toString() result
console.log(cache['[object Object]']); // 'value' — the actual key used

Map eliminates this problem entirely. Keys in a Map can be any type — numbers, strings, booleans, objects, functions, even other Maps and Sets:

const map = new Map();
map.set(1, 'number one');
map.set('1', 'string one');
map.set(true, 'boolean key');
map.set(undefined, 'undefined key');
map.set(null, 'null key');
map.set(NaN, 'NaN key');
map.set(() => {}, 'function key');
map.set({ id: 1 }, 'object key');
map.set(Symbol('test'), 'symbol key');
 
console.log(map.get(1));     // 'number one'
console.log(map.get('1'));   // 'string one'
console.log(map.size);       // 9 — all distinct keys

Notice that even NaN works as a key, and map.get(NaN) returns the correct value — something impossible with objects since NaN !== NaN in JavaScript.

Hash Table Implementation

Internal Implementation: Hash Tables Under the Hood

Both Map and Set use hash table implementations internally, but with optimizations that plain objects lack. When you call map.set(key, value), the engine computes a hash of the key and uses it to determine the storage bucket. This gives O(1) average-case lookup time.

V8 (the engine powering Chrome and Node.js) uses a tiered strategy for Map storage:

  • Small Maps (< ~12 entries): Uses a linear array for fast iteration
  • Medium Maps: Switches to a hash table when the threshold is exceeded
  • Large Maps: Uses a hash table with optimized collision handling

This means that for small collections, Map may actually be faster than a plain object because the engine can optimize the internal representation. For large collections with frequent insertions and deletions, Map maintains consistent performance while plain objects may degrade due to hidden class transitions in V8.

Iteration Order Guarantees

Map and Set guarantee insertion-order iteration — a critical property that plain objects lacked until ES2015 (and still handle inconsistently for integer-like keys):

const map = new Map();
map.set('c', 3);
map.set('a', 1);
map.set('b', 2);
 
// Guaranteed insertion order
for (const [key, value] of map) {
  console.log(key, value); // c 3, a 1, b 2
}
 
// Object comparison — integer keys are sorted first!
const obj = { c: 3, a: 1, b: 2, 0: 'zero' };
console.log(Object.keys(obj)); // ['0', 'c', 'a', 'b']

This ordering guarantee makes Map ideal for scenarios where insertion order matters, such as LRU caches, ordered registries, and configuration priority systems.

Map: Deep Dive

Complete API Surface

Map provides a rich set of methods for managing key-value pairs:

const map = new Map();
 
// Core operations
map.set('key', 'value');           // Returns the Map (chainable)
map.get('key');                     // Returns 'value' or undefined
map.has('key');                     // Returns boolean
map.delete('key');                  // Returns boolean (true if existed)
map.clear();                        // Removes all entries
 
// Size
map.size;                           // Number of entries
 
// Iteration methods
map.keys();                         // Iterator of keys
map.values();                       // Iterator of values
map.entries();                      // Iterator of [key, value] pairs
map.forEach((value, key, map) => {  // Callback iteration
  console.log(key, value);
});
 
// Construction from iterables
const fromArray = new Map([['a', 1], ['b', 2]]);
const fromObject = new Map(Object.entries({ a: 1, b: 2 }));
const fromAnotherMap = new Map(existingMap);
const fromSet = new Map([...set].map(v => [v, v * 2]));

Chaining and Fluent APIs

Map's set() method returns the Map itself, enabling fluent chaining:

const config = new Map()
  .set('host', 'localhost')
  .set('port', 3000)
  .set('debug', true)
  .set('maxRetries', 3);

Map with Complex Keys

The real power of Map shines when using objects as keys:

const componentState = new Map();
 
// Use DOM elements as keys
const header = document.querySelector('header');
const footer = document.querySelector('footer');
 
componentState.set(header, { scrollY: 0, isVisible: true });
componentState.set(footer, { lastUpdated: Date.now() });
 
// Use class instances as keys
class EventEmitter {
  constructor() {
    this.handlers = new Map();
  }
 
  on(event, handler) {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event).add(handler);
    return this;
  }
 
  emit(event, ...args) {
    if (this.handlers.has(event)) {
      for (const handler of this.handlers.get(event)) {
        handler(...args);
      }
    }
    return this;
  }
}

Performance Benchmarking

Set: Deep Dive

Unique Value Collection

Set stores unique values and provides O(1) lookup time, making it far superior to Array.includes() for large collections:

// Deduplication
const numbers = [1, 2, 3, 2, 1, 4, 5, 4, 3];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4, 5]
 
// Performance comparison for membership testing
const largeArray = Array.from({ length: 100000 }, (_, i) => i);
const largeSet = new Set(largeArray);
 
// Array.includes: O(n) — scans linearly
console.time('array');
largeArray.includes(99999);
console.timeEnd('array'); // ~0.1ms
 
// Set.has: O(1) — hash lookup
console.time('set');
largeSet.has(99999);
console.timeEnd('set'); // ~0.001ms

Set Operations: Union, Intersection, Difference

Modern JavaScript (ES2025+) introduces built-in Set methods, but here are both the modern and polyfill approaches:

const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([4, 5, 6, 7, 8]);
 
// === ES2025+ Built-in Methods ===
const union = setA.union(setB);                    // {1,2,3,4,5,6,7,8}
const intersection = setA.intersection(setB);       // {4,5}
const difference = setA.difference(setB);           // {1,2,3}
const symmetricDiff = setA.symmetricDifference(setB); // {1,2,3,6,7,8}
const isSubset = setA.isSubsetOf(setB);             // false
const isSuperset = setA.isSupersetOf(setB);         // false
const hasOverlap = setA.intersection(setB).size > 0; // true
 
// === Polyfill / Pre-ES2025 ===
function setUnion(a, b) {
  return new Set([...a, ...b]);
}
 
function setIntersection(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}
 
function setDifference(a, b) {
  return new Set([...a].filter(x => !b.has(x)));
}
 
function setSymmetricDifference(a, b) {
  return new Set([...a].filter(x => !b.has(x)).concat([...b].filter(x => !a.has(x))));
}

Real-World Set Patterns

// Tag filtering system
const articles = [
  { id: 1, title: 'Intro to JS', tags: ['javascript', 'beginner'] },
  { id: 2, title: 'Advanced CSS', tags: ['css', 'advanced'] },
  { id: 3, title: 'React Patterns', tags: ['javascript', 'react', 'advanced'] },
];
 
function filterByTags(articles, requiredTags) {
  const required = new Set(requiredTags);
  return articles.filter(article => {
    const articleTags = new Set(article.tags);
    return [...required].every(tag => articleTags.has(tag));
  });
}
 
// Permission checking
class PermissionChecker {
  constructor() {
    this.roles = new Map(); // userId -> Set<permission>
  }
 
  assignRole(userId, permissions) {
    this.roles.set(userId, new Set(permissions));
  }
 
  hasPermission(userId, permission) {
    return this.roles.get(userId)?.has(permission) ?? false;
  }
 
  hasAnyPermission(userId, permissions) {
    const userPerms = this.roles.get(userId);
    if (!userPerms) return false;
    return permissions.some(p => userPerms.has(p));
  }
}

WeakMap: Memory-Safe Object Metadata

The Garbage Collection Problem

When you use a regular Map with object keys, the Map holds a strong reference to those objects, preventing garbage collection even when nothing else references them. In long-running applications (SPAs, servers), this causes memory leaks:

// Memory leak with regular Map
const metadata = new Map();
 
function processElement(element) {
  metadata.set(element, { processed: true, timestamp: Date.now() });
  // Even after 'element' is removed from the DOM,
  // the Map keeps it alive in memory!
}
 
// Memory-safe with WeakMap
const safeMetadata = new WeakMap();
 
function processElementSafe(element) {
  safeMetadata.set(element, { processed: true, timestamp: Date.now() });
  // When 'element' is garbage collected, this entry disappears automatically
}

WeakMap only accepts object keys (no primitives), and its references are weak — if no other reference to the key object exists, the garbage collector can reclaim the entry.

Private Data Pattern

WeakMap is the canonical way to implement truly private instance variables in JavaScript classes:

const _private = new WeakMap();
 
class BankAccount {
  constructor(owner, balance) {
    _private.set(this, { owner, balance, transactions: [] });
  }
 
  get balance() {
    return _private.get(this).balance;
  }
 
  deposit(amount) {
    const data = _private.get(this);
    data.balance += amount;
    data.transactions.push({ type: 'deposit', amount, date: new Date() });
    return this;
  }
 
  withdraw(amount) {
    const data = _private.get(this);
    if (amount > data.balance) throw new Error('Insufficient funds');
    data.balance -= amount;
    data.transactions.push({ type: 'withdrawal', amount, date: new Date() });
    return this;
  }
 
  getTransactionHistory() {
    return [..._private.get(this).transactions];
  }
}
 
const account = new BankAccount('Alice', 1000);
account.deposit(500).withdraw(200);
console.log(account.balance); // 1300
// No way to access _private from outside!

DOM Element Metadata

One of the most practical WeakMap use cases is attaching metadata to DOM elements without polluting them:

const elementData = new WeakMap();
 
function trackElement(element) {
  elementData.set(element, {
    clicks: 0,
    lastInteraction: null,
    isVisible: false,
    observer: new IntersectionObserver(([entry]) => {
      elementData.get(element).isVisible = entry.isIntersecting;
    }),
  });
 
  elementData.get(element).observer.observe(element);
 
  element.addEventListener('click', () => {
    const data = elementData.get(element);
    data.clicks++;
    data.lastInteraction = Date.now();
  });
}
 
// When element is removed from DOM and no JS references it,
// the WeakMap entry is automatically garbage collected

Memory Management

WeakSet: Object-Only Unique Tracking

Circular Reference Detection

WeakSet excels at tracking visited objects during deep operations:

const visited = new WeakSet();
 
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (visited.has(obj)) {
    // Circular reference detected — return reference instead of infinite loop
    return obj;
  }
 
  visited.add(obj);
  const clone = Array.isArray(obj) ? [] : {};
 
  for (const [key, value] of Object.entries(obj)) {
    clone[key] = deepClone(value);
  }
 
  return clone;
}
 
// Works with deeply nested circular structures
const data = { a: 1 };
data.self = data; // Circular reference
const cloned = deepClone(data); // Doesn't stack overflow

Object Pool Management

class ObjectPool {
  constructor(factory, reset, maxSize = 100) {
    this.factory = factory;
    this.reset = reset;
    this.maxSize = maxSize;
    this.available = [];
    this.inUse = new WeakSet();
  }
 
  acquire() {
    let obj;
 
    if (this.available.length > 0) {
      obj = this.available.pop();
    } else {
      obj = this.factory();
    }
 
    this.inUse.add(obj);
    return obj;
  }
 
  release(obj) {
    if (!this.inUse.has(obj)) return false;
    this.inUse.delete(obj);
    this.reset(obj);
    if (this.available.length < this.maxSize) {
      this.available.push(obj);
    }
    return true;
  }
 
  get isAvailable() {
    return this.available.length > 0;
  }
}
 
// Usage: DOM element pool
const buttonPool = new ObjectPool(
  () => document.createElement('button'),
  (btn) => {
    btn.textContent = '';
    btn.className = '';
    btn.onclick = null;
  }
);

Performance Benchmarks: Choosing the Right Collection

Lookup Performance

const iterations = 1_000_000;
 
// Build test data
const obj = {};
const map = new Map();
const arr = [];
const set = new Set();
 
for (let i = 0; i < iterations; i++) {
  obj[i] = i;
  map.set(i, i);
  arr.push(i);
  set.add(i);
}
 
// Object property access
console.time('Object lookup');
for (let i = 0; i < iterations; i++) obj[i];
console.timeEnd('Object lookup');
 
// Map.get
console.time('Map lookup');
for (let i = 0; i < iterations; i++) map.get(i);
console.timeEnd('Map lookup');
 
// Array.includes (worst case — O(n))
console.time('Array.includes');
for (let i = 0; i < 10000; i++) arr.includes(iterations - 1 - i);
console.timeEnd('Array.includes');
 
// Set.has
console.time('Set.has');
for (let i = 0; i < 10000; i++) set.has(iterations - 1 - i);
console.timeEnd('Set.has');

Insertion and Deletion Performance

// Map vs Object for frequent mutations
const mapMut = new Map();
const objMut = {};
 
console.time('Map set+delete');
for (let i = 0; i < 100000; i++) {
  mapMut.set(i, i);
  if (i % 3 === 0) mapMut.delete(i - 1);
}
console.timeEnd('Map set+delete');
 
console.time('Object set+delete');
for (let i = 0; i < 100000; i++) {
  objMut[i] = i;
  if (i % 3 === 0) delete objMut[i - 1];
}
console.timeEnd('Object set+delete');
// Map typically 2-5x faster for frequent mutations in V8

Summary Table

OperationMapObjectSetArray
Key typesAnyString/SymbolAny valueN/A (index)
LookupO(1)O(1)O(1)O(n)
InsertO(1)O(1)*O(1)O(1) amortized
DeleteO(1)O(1)*O(1)O(n)
IterationInsertion orderUnstable**Insertion orderIndex order
Size.size O(1)Object.keys().length O(n).size O(1).length O(1)
Garbage collected keysNoNoNoN/A

* Objects may trigger hidden class deoptimizations in V8 when frequently adding/deleting properties. ** Object integer keys are sorted numerically; string keys follow insertion order.

Advanced Patterns

LRU Cache with Map

Map's insertion-order guarantee enables a simple, efficient LRU cache:

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }
 
  get(key) {
    if (!this.cache.has(key)) return undefined;
 
    // Move to end (most recently used)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }
 
  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      // Evict oldest entry (first in iteration order)
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
  }
}
 
const lru = new LRUCache(3);
lru.put('a', 1);
lru.put('b', 2);
lru.put('c', 3);
lru.get('a');       // 1, 'a' moves to end
lru.put('d', 4);    // Evicts 'b' (oldest)
console.log(lru.get('b')); // undefined

Memoization with Map

function memoize(fn) {
  const cache = new Map();
 
  return function (...args) {
    // Create a stable key from arguments
    const key = args.length === 1 ? args[0] : JSON.stringify(args);
 
    if (cache.has(key)) return cache.get(key);
 
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}
 
// For functions with object arguments, use a WeakMap
function memoizeObjects(fn) {
  const cache = new WeakMap();
 
  return function (obj) {
    if (cache.has(obj)) return cache.get(obj);
 
    const result = fn.call(this, obj);
    cache.set(obj, result);
    return result;
  };
}

EventEmitter with Map and Set

class EventEmitter {
  constructor() {
    this.listeners = new Map();
  }
 
  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event).add(callback);
 
    // Return unsubscribe function
    return () => {
      this.listeners.get(event)?.delete(callback);
      if (this.listeners.get(event)?.size === 0) {
        this.listeners.delete(event);
      }
    };
  }
 
  once(event, callback) {
    const unsubscribe = this.on(event, (...args) => {
      unsubscribe();
      callback(...args);
    });
    return unsubscribe;
  }
 
  emit(event, ...args) {
    if (!this.listeners.has(event)) return false;
 
    for (const callback of this.listeners.get(event)) {
      callback(...args);
    }
    return true;
  }
 
  removeAllListeners(event) {
    if (event) {
      this.listeners.delete(event);
    } else {
      this.listeners.clear();
    }
  }
}

State Management Store

class ReactiveStore {
  constructor(initialState = {}) {
    this.state = new Map(Object.entries(initialState));
    this.listeners = new Map();
    this.computedCache = new Map();
  }
 
  get(key) {
    return this.state.get(key);
  }
 
  set(key, value) {
    const oldValue = this.state.get(key);
    if (oldValue === value) return;
 
    this.state.set(key, value);
    this.computedCache.clear(); // Invalidate computed values
 
    // Notify subscribers
    if (this.listeners.has(key)) {
      for (const listener of this.listeners.get(key)) {
        listener(value, oldValue, key);
      }
    }
 
    // Notify wildcard subscribers
    if (this.listeners.has('*')) {
      for (const listener of this.listeners.get('*')) {
        listener(value, oldValue, key);
      }
    }
  }
 
  subscribe(key, listener) {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, new Set());
    }
    this.listeners.get(key).add(listener);
    return () => this.listeners.get(key).delete(listener);
  }
 
  // Computed values with caching
  compute(key, deps, computeFn) {
    const depValues = deps.map(d => this.state.get(d));
    const cacheKey = `${key}:${depValues.join(',')}`;
 
    if (this.computedCache.has(cacheKey)) {
      return this.computedCache.get(cacheKey);
    }
 
    const result = computeFn(...depValues);
    this.computedCache.set(cacheKey, result);
    return result;
  }
}

Common Pitfalls and Solutions

Pitfall 1: Object Key Coercion

// Wrong: Object key coercion
const cache = {};
cache[{ id: 1 }] = 'value';
console.log(cache[{ id: 1 }]); // undefined — different toString()!
 
// Right: Use Map
const map = new Map();
const key = { id: 1 };
map.set(key, 'value');
console.log(map.get(key)); // 'value'

Pitfall 2: Memory Leaks with Map

// Wrong: Map holding references to removed DOM elements
const elementMap = new Map();
function process(el) {
  elementMap.set(el, { processed: true });
  el.remove(); // Element removed from DOM, but Map keeps it alive!
}
 
// Right: Use WeakMap
const elementWeakMap = new WeakMap();
function processSafe(el) {
  elementWeakMap.set(el, { processed: true });
  el.remove(); // Element will be garbage collected when no other refs exist
}

Pitfall 3: Comparing Object References

const map = new Map();
map.set({ id: 1 }, 'Alice');
map.set({ id: 1 }, 'Bob');
 
console.log(map.size); // 2 — different object references!
 
// Solution: Use a string key or maintain a reference
const aliceKey = { id: 1 };
map.set(aliceKey, 'Alice');
console.log(map.get(aliceKey)); // 'Alice'

Pitfall 4: WeakMap/WeakSet Limitations

const weakMap = new WeakMap();
weakMap.set({}, 'value');
 
// These do NOT work:
// weakMap.size          // undefined
// weakMap.keys()        // TypeError
// weakMap.forEach()     // TypeError
// for (const x of weakMap) {} // TypeError
 
// Workaround: Use a regular Map when you need iteration
// Use WeakMap only when garbage collection of keys is essential

Serialization and Storage

Map and Set cannot be directly serialized to JSON. Here are reliable conversion patterns:

// Map serialization
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
 
// To JSON
const mapJSON = JSON.stringify(Object.fromEntries(map));
// '{"a":1,"b":2,"c":3}'
 
// From JSON (works for string keys only)
const restored = new Map(Object.entries(JSON.parse(mapJSON)));
 
// For non-string keys, use array format
const mapArray = JSON.stringify([...map]);
const restoredFromArr = new Map(JSON.parse(mapArray));
 
// Set serialization
const set = new Set([1, 2, 3, 4, 5]);
const setJSON = JSON.stringify([...set]);
const restoredSet = new Set(JSON.parse(setJSON));
 
// localStorage integration
function saveMap(key, map) {
  localStorage.setItem(key, JSON.stringify([...map]));
}
 
function loadMap(key) {
  const data = localStorage.getItem(key);
  return data ? new Map(JSON.parse(data)) : new Map();
}

Testing Strategies

describe('Map and Set collections', () => {
  describe('Map', () => {
    it('should store and retrieve key-value pairs with any key type', () => {
      const map = new Map();
      const objKey = { id: 1 };
      map.set(42, 'number');
      map.set(objKey, 'object');
      map.set(true, 'boolean');
 
      expect(map.get(42)).toBe('number');
      expect(map.get(objKey)).toBe('object');
      expect(map.get(true)).toBe('boolean');
      expect(map.size).toBe(3);
    });
 
    it('should maintain insertion order', () => {
      const map = new Map();
      map.set('z', 1);
      map.set('a', 2);
      map.set('m', 3);
      expect([...map.keys()]).toEqual(['z', 'a', 'm']);
    });
 
    it('should handle NaN as key', () => {
      const map = new Map();
      map.set(NaN, 'not a number');
      expect(map.get(NaN)).toBe('not a number');
    });
  });
 
  describe('Set', () => {
    it('should deduplicate values', () => {
      const set = new Set([1, 2, 2, 3, 3, 3]);
      expect([...set]).toEqual([1, 2, 3]);
    });
 
    it('should perform set operations', () => {
      const a = new Set([1, 2, 3]);
      const b = new Set([2, 3, 4]);
 
      const union = new Set([...a, ...b]);
      const intersection = new Set([...a].filter(x => b.has(x)));
      const difference = new Set([...a].filter(x => !b.has(x)));
 
      expect([...union]).toEqual([1, 2, 3, 4]);
      expect([...intersection]).toEqual([2, 3]);
      expect([...difference]).toEqual([1]);
    });
  });
 
  describe('WeakMap', () => {
    it('should allow garbage collection of keys', () => {
      let obj = { data: 'test' };
      const weakMap = new WeakMap();
      weakMap.set(obj, 'metadata');
 
      expect(weakMap.get(obj)).toBe('metadata');
      obj = null; // Object can now be garbage collected
      // No way to verify GC in a test, but this demonstrates the pattern
    });
  });
 
  describe('WeakSet', () => {
    it('should track object membership without preventing GC', () => {
      const visited = new WeakSet();
      const node1 = { id: 1 };
      const node2 = { id: 2 };
 
      visited.add(node1);
      expect(visited.has(node1)).toBe(true);
      expect(visited.has(node2)).toBe(false);
    });
  });
});

When to Use Each Collection

ScenarioBest ChoiceWhy
Key-value pairs with string keys, staticObjectSimpler syntax, JSON-serializable
Key-value pairs with non-string keysMapNo key coercion, any type supported
Frequent add/delete operationsMapOptimized for mutations
Need insertion-order guaranteeMapReliable iteration order
Unique values / deduplicationSetO(1) has, automatic dedup
Set operations (union, intersection)SetClean API (ES2025+ built-in methods)
Attach metadata to objectsWeakMapPrevents memory leaks
Track visited objects in traversalWeakSetPrevents memory leaks
Need to iterate over entriesMap or SetWeakMap/WeakSet are not iterable
Need .size propertyMap or SetWeakMap/WeakSet have no size
Serializable to JSONObject or Map (via conversion)Weak variants cannot be serialized

Conclusion

JavaScript's collection types provide powerful alternatives to plain objects and arrays. Each collection has a specific niche:

  • Map — The go-to for key-value pairs when keys are non-string types, when you need guaranteed iteration order, or when the collection undergoes frequent mutations.
  • Set — Essential for uniqueness guarantees, membership testing, and set-theoretic operations on collections of values.
  • WeakMap — The correct choice for associating metadata with objects without preventing garbage collection. Use it for private data, DOM metadata, and caching.
  • WeakSet — Ideal for tracking object membership (visited nodes, processed items) without memory leak risks.

Key takeaways:

  1. Map supports any key type and maintains insertion order — use it over objects for dynamic collections
  2. Set provides O(1) membership testing and automatic deduplication — use it over Array.includes()
  3. WeakMap and WeakSet prevent memory leaks for object references — essential in long-running applications
  4. Choose based on key types, iteration needs, serialization requirements, and memory concerns
  5. V8 optimizes Map/Set internally with tiered hash table strategies for best performance

Explore collections further in the MDN documentation and practice with interactive examples to build intuition for when each collection shines.