DEV Community

Alex Chen
Alex Chen

Posted on

JavaScript Map, Set, WeakMap, and WeakSet: When to Use Which

JavaScript Map, Set, WeakMap, and WeakSet: When to Use Which

Stop using plain objects for everything. Here's what to use when.

Quick Comparison

┌───────────┬──────────┬──────────┬──────────┬────────────┐
│           │   Map    │   Set    │  WeakMap  │  WeakSet   │
├───────────┼──────────┼──────────┼──────────┼────────────┤
│ Key type  │ Any value│ N/A      │ Objects  │ N/A        │
│ Value     │ Any value│ Keys only│ Any value│ Objects    │
│ Iterable?│ ✅       │ ✅       │ ❌       │ ❌         │
│ Size prop│ ✅       │ ✅       │ ❌       │ ❌         │
│ GC safe  │ No       │ No       │ Yes      │ Yes        │
│ Use case │ Lookup   │ Unique   │ Private  │ Membership │
└───────────┴──────────┴──────────┴──────────┴────────────┘
Enter fullscreen mode Exit fullscreen mode

Map (Key-Value Store with Advantages Over Objects)

// ❌ Object limitations:
const obj = {};
obj['toString'] = 'oops'; // Overrides inherited method!
obj.__proto__ = 'dangerous'; // Can pollute prototype
Object.keys(obj).length; // Doesn't count non-enumerable properties

// ✅ Map advantages:
const map = new Map();

// Any type as key (not just strings/symbols!)
map.set('name', 'Alex');
map.set(42, 'the answer');
map.set(true, 'boolean key');
map.set({}, 'object key');
map.set(() => {}, 'function key');
map.set(NaN, 'NaN works!'); // NaN === NaN in Maps!

// Get values
map.get('name');           // 'Alex'
map.get('nonexistent');    // undefined

// Check existence
map.has('name');           // true
map.has('toString');       // false (no prototype pollution!)

// Size
map.size;                  // 6

// Delete
map.delete(42);            // true
map.clear();               // Remove all

// Iterate (insertion order preserved!)
for (const [key, value] of map) {
  console.log(`${key} = ${value}`);
}

// Convert to/from arrays
const arr = Array.from(map); // [[key, value], ...]
const newMap = new Map([['a', 1], ['b', 2]]);

// Practical: LRU Cache
class LRUCache {
  #cache = new Map();

  constructor(maxSize = 100) {
    this.maxSize = maxSize;
  }

  get(key) {
    if (!this.#cache.has(key)) return undefined;
    const value = this.#cache.get(key);
    this.#cache.delete(key);     // Remove and re-add to mark as recently used
    this.#cache.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.#cache.has(key)) this.#cache.delete(key);
    if (this.#cache.size >= this.maxSize) {
      const oldest = this.#cache.keys().next().value;
      this.#cache.delete(oldest);
    }
    this.#cache.set(key, value);
  }
}
Enter fullscreen mode Exit fullscreen mode

Set (Unique Values Only)

// Create from array (removes duplicates!)
const numbers = [1, 2, 2, 3, 3, 3, 4, 5];
const unique = [...new Set(numbers)]; // [1, 2, 3, 4, 5]

const set = new Set();

set.add('hello');
set.add('world');
set.add('hello');           // Ignored — already exists!
set.size;                  // 2

set.has('world');           // true
set.delete('hello');        // true

// Useful operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

// Union
new Set([...a, ...b]);      // {1, 2, 3, 4}

// Intersection
new Set([...a].filter(x => b.has(x))); // {2, 3}

// Difference
new Set([...a].filter(x => !b.has(x))); // {1}

// Practical use cases:

// Deduplicate array of objects by property
const users = [
  { id: 1, name: 'Alex' },
  { id: 2, name: 'Sam' },
  { id: 1, name: 'Alex' }, // Duplicate!
];
const uniqueUsers = [...new Map(users.map(u => [u.id, u])).values()];

// Check for unique characters
const allUnique = str => new Set(str).size === str.length;
allUnique('abcde');  // true
allUnique('hello');  // false (l repeats)
Enter fullscreen mode Exit fullscreen mode

WeakMap (Garbage-Collected Key-Value)

// Keys MUST be objects (or non-registered symbols)
// Values can be anything
// NOT iterable (can't list keys/values)
// Keys are weakly held — GC can collect them!

// Practical: Private class fields (before #private was available)
class User {
  #data = new WeakMap();

  constructor(name, password) {
    this.name = name;
    this.#data.set(this, { password }); // `this` is the key!
  }

  checkPassword(input) {
    return this.#data.get(this)?.password === input;
  }
}

// When the User instance is garbage collected,
// the password data is automatically cleaned up!

// Practical: Metadata without memory leaks
const elementData = new WeakMap();

function addMetadata(el, data) {
  elementData.set(el, data);
}

// Even if you remove the element from DOM,
// the data won't prevent GC from collecting it!

// Practical: Caching expensive computations on objects
const cache = new WeakMap();

function deepClone(obj) {
  if (cache.has(obj)) return cache.get(obj);

  const clone = /* expensive clone operation */;
  cache.set(obj, clone);
  return clone;
}

// Cache automatically cleans up when original objects are GC'd!
Enter fullscreen mode Exit fullscreen mode

WeakSet (Garbage-Collected Membership)

// Only stores objects
// Can't iterate or get size
// Perfect for "is this object tracked?" checks

const processed = new WeakSet();

function processItem(item) {
  if (processed.has(item)) {
    console.log('Already processed, skipping');
    return;
  }

  // Process item...
  processed.add(item); // Mark as processed
}

// When item is no longer referenced elsewhere,
// it's automatically removed from the WeakSet!

// Practical: DOM element tracking
const clickedElements = new WeakSet();

document.addEventListener('click', e => {
  if (clickedElements.has(e.target)) {
    e.target.classList.toggle('selected');
  } else {
    clickedElements.add(e.target);
    e.target.classList.add('clicked-once');
  }
});

// Practical: Prevent circular references causing memory leaks
const visited = new WeakSet();

function traverse(obj) {
  if (typeof obj !== 'object' || obj === null || visited.has(obj)) return;
  visited.add(obj);

  for (const key of Object.keys(obj)) {
    traverse(obj[key]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Performance

// Map vs Object for frequent add/delete:
// Map is faster for large datasets, especially with frequent additions/deletions

// Benchmark (approximate):
// 10,000 entries:
// - Object: ~15ms for lookup, ~25ms for delete
// - Map: ~8ms for lookup, ~12ms for delete

// Set vs Array for membership testing:
// 10,000 items, 10,000 lookups:
// - Array.includes(): ~120ms
// - Set.has(): ~0.05ms (2400x faster!)

// Always use Set for frequent "is X in collection?" checks!
Enter fullscreen mode Exit fullscreen mode

Decision Guide

Need key-value pairs?
├── Keys are strings/symbols → Object {} or Map
│   ├── Need JSON serialization → Object {}
│   ├── Need any-type keys → **Map**
│   ├── Need ordered iteration → **Map**
│   └── Need prototype-free → **Map**
│
├── Keys are objects + auto-GC → **WeakMap**
│   (Private data, metadata, caches)
│
Need unique values only?
├── Need to iterate → **Set**
├── Just test membership → **Set** (for speed!) or **WeakSet** (if objects + GC)
│
Storing DOM-related data?
├── Need access later → **WeakMap** (element → data)
├── Just track membership → **WeakSet** (clicked elements, etc.)
Enter fullscreen mode Exit fullscreen mode

Which one do you use most? Any creative use cases?

Follow @armorbreak for more JavaScript content.

Top comments (0)