JavaScript Map & Set Mastery: Complete Guide to Modern Data Structures
Master Map, Set, WeakMap, and WeakSet. Learn when to use Map vs Object, Set operations like union and intersection, real-world patterns like LRU caches, and performance comparisons.
- 1. JavaScript Type Coercion Mastery: A Senior Engineer's Guide to the Chaos
- 2. JavaScript Runtime Deep Dive: Event Loop, Call Stack & Async Execution
- 3. Mastering JavaScript Callbacks: From Sync to Async & The Event Loop
- 4. JavaScript Closures Deep Dive: Mastering Lexical Scope & Memory
- 5. JavaScript Object Prototyping Mastery: A Visual Guide to Inheritance
- 6. Demystifying the JavaScript 'this' Keyword: A Visual Guide
- 7. JavaScript Promises & Promise Chaining Mastery: From Callbacks to Async Flow
- 8. JavaScript Array Methods Mastery: Complete Guide from map() to reduce()
- 9. JavaScript Map & Set Mastery: Complete Guide to Modern Data Structures
- 10. JavaScript String Methods Mastery: Complete Guide from slice() to replaceAll()
- 11. JavaScript Regular Expressions Mastery: Real-World Patterns & Validation
Introduction
JavaScript objects and arrays are the most common data structures, but they have limitations. Objects can only use strings and symbols as keys. Arrays don't enforce uniqueness. When you need key-value pairs with any type of key, or collections of unique values, you need Map and Set.
Introduced in ES2015, Map and Set are purpose-built data structures that solve specific problems better than objects and arrays. They're widely used in caching, deduplication, graph algorithms, DOM tracking, and more.
This guide covers everything you need to know—from basic operations to advanced patterns, WeakMap/WeakSet, real-world use cases, and interview questions.
Map vs Object vs Set vs Array — At a Glance
Common Methods: Map vs Set
Both Map and Set share a similar API surface. Here's how they compare:
| Operation | Map | Set | Notes |
|---|---|---|---|
| Add entry | map.set(key, value) | set.add(value) | Both are chainable and return the collection itself |
| Check existence | map.has(key) | set.has(value) | Both return boolean, both O(1) |
| Remove entry | map.delete(key) | set.delete(value) | Both return boolean (was found?) |
| Remove all | map.clear() | set.clear() | Both return undefined |
| Count entries | map.size | set.size | Both are properties, not methods |
| Get value | map.get(key) | ❌ Not available | Sets don't have key-value pairs |
| Iterate keys | map.keys() | set.keys() | Set keys and values are the same |
| Iterate values | map.values() | set.values() | Set values() is the default iterator |
| Iterate entries | map.entries() → [key, value] | set.entries() → [value, value] | Set entries repeat the value as both key and value |
| forEach | map.forEach((val, key) => {}) | set.forEach((val, val2) => {}) | Set callback receives value twice (for API consistency) |
| for...of | for (const [k, v] of map) | for (const v of set) | Map defaults to entries, Set defaults to values |
| Spread | [...map] → [[k,v], ...] | [...set] → [v, ...] | Easy conversion to arrays |
| Maintains order | ✅ Insertion order | ✅ Insertion order | Both guarantee iteration in insertion order |
| Key equality | SameValueZero | SameValueZero | NaN === NaN is true in both |
ℹ️ note[!NOTE] Set's
keys(),values(), andentries()exist for API consistency with Map. In practice, just usefor...ofor spread with Sets.
1. Map — Key-Value Pairs with Any Key Type
1.1 Creating a Map
// Empty Map
const map = new Map();
// From array of [key, value] pairs
const userRoles = new Map([
['alice', 'admin'],
['bob', 'editor'],
['charlie', 'viewer']
]);
console.log(userRoles);
// Map(3) { 'alice' => 'admin', 'bob' => 'editor', 'charlie' => 'viewer' }
1.2 Core Methods
| Method | Description | Returns |
|---|---|---|
set(key, value) | Add or update a key-value pair | The Map itself (chainable) |
get(key) | Get value by key | Value or undefined |
has(key) | Check if key exists | boolean |
delete(key) | Remove a key-value pair | boolean (was it found?) |
clear() | Remove all entries | undefined |
size | Number of entries (property, not method) | number |
const map = new Map();
// set() — returns the Map (chainable!)
map.set('name', 'Alice')
.set('age', 30)
.set('active', true);
// get()
console.log(map.get('name')); // 'Alice'
console.log(map.get('missing')); // undefined
// has()
console.log(map.has('age')); // true
console.log(map.has('email')); // false
// size
console.log(map.size); // 3
// delete()
map.delete('active');
console.log(map.size); // 2
// clear()
map.clear();
console.log(map.size); // 0
1.3 Any Type as Key
This is the killer feature of Map. Unlike objects, Map keys can be anything:
const map = new Map();
// Object as key
const user = { id: 1, name: 'Alice' };
map.set(user, 'premium');
// Function as key
const greet = () => 'Hello';
map.set(greet, 'greeting function');
// Number as key (not converted to string!)
map.set(1, 'one');
map.set('1', 'string one'); // Different key from above!
console.log(map.get(1)); // 'one'
console.log(map.get('1')); // 'string one'
console.log(map.get(user)); // 'premium'
// DOM elements as keys
const button = document.querySelector('#submit');
map.set(button, { clicks: 0, lastClicked: null });
💜 important[!IMPORTANT] Map uses the SameValueZero algorithm for key comparison. This means
NaN === NaNistruein Maps (unlike regular===), and-0equals+0.
const map = new Map();
map.set(NaN, 'not a number');
console.log(map.get(NaN)); // 'not a number' ✅ (NaN === NaN in Maps)
1.4 Iterating Over a Map
Maps maintain insertion order and provide multiple iteration methods:
const fruits = new Map([
['apple', 1.5],
['banana', 0.75],
['cherry', 3.0]
]);
// forEach()
fruits.forEach((value, key) => {
console.log(`${key}: $${value}`);
});
// for...of (default: entries)
for (const [key, value] of fruits) {
console.log(`${key}: $${value}`);
}
// keys()
for (const key of fruits.keys()) {
console.log(key); // 'apple', 'banana', 'cherry'
}
// values()
for (const value of fruits.values()) {
console.log(value); // 1.5, 0.75, 3.0
}
// entries()
for (const [key, value] of fruits.entries()) {
console.log(`${key} = ${value}`);
}
Converting Map to Array:
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const keys = [...map.keys()]; // ['a', 'b', 'c']
const values = [...map.values()]; // [1, 2, 3]
const entries = [...map.entries()]; // [['a', 1], ['b', 2], ['c', 3]]
const entries2 = [...map]; // Same as above
1.5 Map vs Object — When to Use Which
| Feature | Map | Object |
|---|---|---|
| Key types | Any (objects, functions, primitives) | Strings & Symbols only |
| Key order | Guaranteed insertion order | Mostly ordered (integers sorted first) |
| Size | map.size | Object.keys(obj).length |
| Iteration | Directly iterable (for...of) | Need Object.keys() / Object.entries() |
| Performance | Better for frequent add/delete | Better for static data |
| Default keys | None | Has prototype keys (toString, etc.) |
| Serialization | Not JSON-serializable by default | JSON-serializable |
| Destructuring | Not directly supported | Supported |
💡 tip[!TIP] Use Map when: keys are dynamic/unknown, keys are non-strings, you need frequent additions/deletions, you need to know the
size, or order matters.Use Object when: keys are known and static, you need JSON serialization, you need destructuring, or you're working with APIs that expect plain objects.
1.6 Map Serialization (JSON)
Maps aren't directly JSON-serializable, but conversion is easy:
const map = new Map([['name', 'Alice'], ['age', 30]]);
// Map → JSON
const json = JSON.stringify([...map]);
console.log(json); // '[["name","Alice"],["age",30]]'
// JSON → Map
const restored = new Map(JSON.parse(json));
console.log(restored.get('name')); // 'Alice'
// Map → Object (string keys only)
const obj = Object.fromEntries(map);
console.log(obj); // { name: 'Alice', age: 30 }
// Object → Map
const map2 = new Map(Object.entries(obj));
2. Set — Collections of Unique Values
2.1 Creating a Set
// Empty Set
const set = new Set();
// From array (duplicates removed automatically!)
const unique = new Set([1, 2, 3, 2, 1, 3]);
console.log(unique); // Set(3) { 1, 2, 3 }
// From string
const chars = new Set('hello');
console.log(chars); // Set(4) { 'h', 'e', 'l', 'o' }
2.2 Core Methods
| Method | Description | Returns |
|---|---|---|
add(value) | Add a value | The Set itself (chainable) |
has(value) | Check if value exists | boolean |
delete(value) | Remove a value | boolean (was it found?) |
clear() | Remove all values | undefined |
size | Number of values (property) | number |
const colors = new Set();
// add() — chainable
colors.add('red')
.add('green')
.add('blue')
.add('red'); // Ignored! Already exists
console.log(colors.size); // 3
// has() — O(1) lookup!
console.log(colors.has('red')); // true
console.log(colors.has('yellow')); // false
// delete()
colors.delete('green');
console.log(colors.size); // 2
// clear()
colors.clear();
console.log(colors.size); // 0
ℹ️ note[!NOTE]
Set.has()is O(1) average time complexity, compared toArray.includes()which is O(n). This makes Sets significantly faster for large collections.
2.3 Iterating Over a Set
const fruits = new Set(['apple', 'banana', 'cherry']);
// for...of
for (const fruit of fruits) {
console.log(fruit);
}
// forEach
fruits.forEach(value => console.log(value));
// Convert to Array
const arr = [...fruits];
// or
const arr2 = Array.from(fruits);
Sets maintain insertion order:
const set = new Set();
set.add('c').add('a').add('b');
console.log([...set]); // ['c', 'a', 'b'] (insertion order)
2.4 Array Deduplication with Set
The most common use case for Sets:
// Simple deduplication
const numbers = [1, 2, 3, 2, 1, 4, 5, 4];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4, 5]
// Deduplicate objects by property
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' } // Duplicate
];
const uniqueIds = [...new Set(users.map(u => u.id))];
console.log(uniqueIds); // [1, 2]
// Deduplicate and keep objects
const seen = new Set();
const uniqueUsers = users.filter(user => {
if (seen.has(user.id)) return false;
seen.add(user.id);
return true;
});
2.5 Set Operations
Union (All elements from both Sets)
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
const union = new Set([...setA, ...setB]);
console.log(union); // Set { 1, 2, 3, 4, 5, 6 }
// ES2025+ (built-in method)
const union2 = setA.union(setB);
Intersection (Common elements)
const intersection = new Set(
[...setA].filter(x => setB.has(x))
);
console.log(intersection); // Set { 3, 4 }
// ES2025+
const intersection2 = setA.intersection(setB);
Difference (Elements in A but not in B)
const difference = new Set(
[...setA].filter(x => !setB.has(x))
);
console.log(difference); // Set { 1, 2 }
// ES2025+
const difference2 = setA.difference(setB);
Symmetric Difference (Elements in either but not both)
const symDifference = new Set(
[...setA].filter(x => !setB.has(x))
.concat([...setB].filter(x => !setA.has(x)))
);
console.log(symDifference); // Set { 1, 2, 5, 6 }
// ES2025+
const symDifference2 = setA.symmetricDifference(setB);
💡 tip[!TIP] The built-in Set methods (
union(),intersection(),difference(),symmetricDifference()) are available in modern browsers and Node.js 22+. Use the manual implementations for older environments.
3. WeakMap — Maps with Garbage-Collectible Keys
3.1 What Makes WeakMap Special?
const weakMap = new WeakMap();
let user = { name: 'Alice' };
weakMap.set(user, 'some metadata');
console.log(weakMap.get(user)); // 'some metadata'
user = null; // The object can now be garbage collected!
// The WeakMap entry is automatically removed
WeakMap Restrictions:
| Feature | Map | WeakMap |
|---|---|---|
| Key types | Any | Objects only |
| Iterable | ✅ Yes | ❌ No |
| size | ✅ Available | ❌ Not available |
| clear() | ✅ Available | ❌ Not available |
| Garbage collection | Keys prevent GC | Keys don't prevent GC |
3.2 Use Cases
Private Data:
const privateData = new WeakMap();
class Person {
constructor(name, age) {
privateData.set(this, { name, age });
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
}
const alice = new Person('Alice', 30);
console.log(alice.getName()); // 'Alice'
console.log(alice.getAge()); // 30
// No way to access privateData from outside!
Caching/Memoization:
const cache = new WeakMap();
function expensiveOperation(obj) {
if (cache.has(obj)) {
console.log('Cache hit!');
return cache.get(obj);
}
const result = /* ...expensive calculation... */ obj.value * 2;
cache.set(obj, result);
return result;
}
let data = { value: 42 };
expensiveOperation(data); // Computes
expensiveOperation(data); // Cache hit!
data = null; // Cache entry is automatically cleaned up!
DOM Metadata:
const elementData = new WeakMap();
function trackElement(element) {
elementData.set(element, {
clicks: 0,
created: Date.now()
});
}
function recordClick(element) {
const data = elementData.get(element);
if (data) data.clicks++;
}
// When the DOM element is removed, the metadata
// is automatically garbage collected!
4. WeakSet — Sets with Garbage-Collectible Values
4.1 What Makes WeakSet Special?
const weakSet = new WeakSet();
let obj = { name: 'Alice' };
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
obj = null; // Object can be garbage collected
// WeakSet entry is automatically removed
WeakSet has only 3 methods: add(), has(), delete()
4.2 Use Cases
Tracking Visited/Processed Objects:
const visited = new WeakSet();
function processNode(node) {
if (visited.has(node)) {
return; // Already processed — avoid infinite loops!
}
visited.add(node);
// Process the node...
console.log(`Processing: ${node.name}`);
if (node.children) {
node.children.forEach(child => processNode(child));
}
}
Branding / Type Checking:
const validRequests = new WeakSet();
class Request {
constructor(url) {
this.url = url;
validRequests.add(this);
}
}
function processRequest(req) {
if (!validRequests.has(req)) {
throw new Error('Invalid request object!');
}
// Safe to process...
}
const req = new Request('/api/users');
processRequest(req); // ✅ Works
processRequest({ url: '/hack' }); // ❌ Throws Error
5. Real-World Patterns
5.1 Frequency Counter (Map)
function wordFrequency(text) {
const words = text.toLowerCase().match(/\b\w+\b/g) || [];
const freq = new Map();
for (const word of words) {
freq.set(word, (freq.get(word) || 0) + 1);
}
// Sort by frequency (descending)
return new Map(
[...freq.entries()].sort((a, b) => b[1] - a[1])
);
}
const result = wordFrequency('the cat sat on the mat the cat');
console.log(result);
// Map { 'the' => 3, 'cat' => 2, 'sat' => 1, 'on' => 1, 'mat' => 1 }
5.2 LRU Cache (Map)
Map's insertion order makes it perfect for Least Recently Used caches:
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
// 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) {
// Delete oldest (first entry)
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
}
this.cache.set(key, value);
}
}
const cache = new LRUCache(3);
cache.put('a', 1);
cache.put('b', 2);
cache.put('c', 3);
cache.put('d', 4); // Evicts 'a'
console.log(cache.get('b')); // 2 (moves 'b' to end)
5.3 Bi-directional Lookup (Two Maps)
class BiMap {
constructor() {
this.forward = new Map();
this.reverse = new Map();
}
set(key, value) {
this.forward.set(key, value);
this.reverse.set(value, key);
}
getByKey(key) {
return this.forward.get(key);
}
getByValue(value) {
return this.reverse.get(value);
}
}
const countryCode = new BiMap();
countryCode.set('US', 'United States');
countryCode.set('IN', 'India');
console.log(countryCode.getByKey('IN')); // 'India'
console.log(countryCode.getByValue('United States')); // 'US'
5.4 Unique Tag Collector (Set)
function collectTags(posts) {
const allTags = new Set();
posts.forEach(post => {
post.tags.forEach(tag => allTags.add(tag));
});
return [...allTags].sort();
}
const posts = [
{ title: 'Post 1', tags: ['javascript', 'react'] },
{ title: 'Post 2', tags: ['javascript', 'node'] },
{ title: 'Post 3', tags: ['react', 'nextjs'] }
];
console.log(collectTags(posts));
// ['javascript', 'nextjs', 'node', 'react']
5.5 Permission System (Set)
class PermissionManager {
constructor() {
this.permissions = new Map(); // userId → Set of permissions
}
grant(userId, permission) {
if (!this.permissions.has(userId)) {
this.permissions.set(userId, new Set());
}
this.permissions.get(userId).add(permission);
}
revoke(userId, permission) {
this.permissions.get(userId)?.delete(permission);
}
check(userId, permission) {
return this.permissions.get(userId)?.has(permission) ?? false;
}
listPermissions(userId) {
return [...(this.permissions.get(userId) || [])];
}
}
const pm = new PermissionManager();
pm.grant('alice', 'read');
pm.grant('alice', 'write');
pm.grant('alice', 'read'); // Duplicate ignored
console.log(pm.check('alice', 'write')); // true
console.log(pm.listPermissions('alice')); // ['read', 'write']
6. Performance Comparison
6.1 Map vs Object Performance
| Operation | Map | Object |
|---|---|---|
| Insert | O(1) | O(1) |
| Lookup | O(1) | O(1) |
| Delete | O(1) | O(1) (but delete can be slow) |
| Size | O(1) via .size | O(n) via Object.keys().length |
| Iteration | Fast (native iterator) | Slower (create array first) |
6.2 Set vs Array Performance
| Operation | Set | Array |
|---|---|---|
| Add | O(1) | O(1) (push) |
| Has/Includes | O(1) | O(n) |
| Delete | O(1) | O(n) (splice) |
| Size | O(1) | O(1) |
⚠️ warning[!WARNING] For large datasets with frequent lookups or membership checks, always prefer Set over Array. The difference between O(1) and O(n) becomes enormous at scale.
// Benchmark: Checking membership in 1,000,000 items
const arr = Array.from({ length: 1_000_000 }, (_, i) => i);
const set = new Set(arr);
console.time('Array.includes');
arr.includes(999_999);
console.timeEnd('Array.includes'); // ~2ms
console.time('Set.has');
set.has(999_999);
console.timeEnd('Set.has'); // ~0.001ms (1000x faster!)
7. Quick Interview Q&A
| Question | Answer |
|---|---|
| Map vs Object? | Map supports any key type, has guaranteed order, has .size, and is better for dynamic keys. |
| When to use Set? | When you need unique values or fast O(1) membership checks. |
| WeakMap keys garbage collected? | Yes. If no other reference exists to the key object, both the key and value are garbage collected. |
| Is Map iterable? | Yes. Maps are directly iterable with for...of. Objects are not. |
| Set preserves order? | Yes. Sets maintain insertion order. |
| Can Map have NaN as key? | Yes. Map treats NaN === NaN as true (SameValueZero algorithm). |
| How to convert Map to JSON? | JSON.stringify([...map]) or JSON.stringify(Object.fromEntries(map)) for string keys. |
| Set vs Array dedup? | [...new Set(array)] is the cleanest deduplication pattern. |
Conclusion
Map and Set are essential data structures that every JavaScript developer should master:
- Map: Use when you need key-value pairs with non-string keys, guaranteed order, or frequent additions/deletions
- Set: Use when you need unique values or fast membership checks
- WeakMap/WeakSet: Use when keys/values should be garbage-collectible (DOM tracking, caching, private data)
- Performance: Set's O(1)
has()vs Array's O(n)includes()makes a massive difference at scale - Patterns: LRU caches, permission systems, frequency counters, and deduplication are natural use cases
Don't default to objects and arrays for everything. Choose the right data structure, and your code will be cleaner, faster, and more expressive.
Happy coding! 🚀
🧠 Test Your Knowledge
Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.