Map and Set in JavaScript
When we first start with JavaScript, Objects and Arrays feel like they can do everything. We use Arrays for lists and Objects for everything else. But as our apps grow, we hit "speed bumps"—like Objects accidentally converting your keys to strings or Arrays allowing duplicate items when you don't want them.
This is where Map and Set come in. Introduced in ES6, these are specialized "collections" designed to solve the specific headaches of traditional data structures.
Introduction Maps and Sets were added to JavaScript (ES6) to provide better built-in data structures for common patterns: Map for flexible key-value storage, and Set for storing unique values. This post explains what they are, how they differ from Objects and Arrays, when to use them, practical examples, problems with traditional structures, and diagram ideas to visualize them. The audience is beginners who want practical, real-world guidance.
What is Map? A Map is a collection of key-value pairs where:
Keys can be of any type (primitive or object).
The insertion order of keys is preserved.
You can get the number of entries with .size.
You can iterate over entries in insertion order.
Basic usage:
const m = new Map(); m.set('name', 'Alice'); m.set(42, 'the answer'); const objKey = { id: 1 }; m.set(objKey, 'object value');
console.log(m.get('name')); // "Alice" console.log(m.get(42)); // "the answer" console.log(m.get(objKey)); // "object value" console.log(m.size); // 3
for (const [key, value] of m) { console.log(key, '=>', value); }
Notes:
Map keys use SameValueZero equality (NaN is considered equal to NaN).
Keys are compared by identity for objects (reference equality).
What is Set? A Set is a collection of unique values. It:
Stores values of any type.
Ensures each value occurs only once (uniqueness).
Preserves insertion order.
Has .size, .add(), .has(), .delete(), and .clear().
Basic usage:
const s = new Set(); s.add(1); s.add(1); s.add('a'); s.add({ x: 1 });
console.log(s.size); // 3 (1, 'a', and the object) console.log(s.has(1)); // true
for (const value of s) { console.log(value); }
Uniqueness is based on SameValueZero equality (so NaN === NaN for the Set).
Key methods and properties Map:
new Map(), map.set(k, v), map.get(k), map.has(k), map.delete(k), map.clear(), map.size
Iteration: map.keys(), map.values(), map.entries(), for (const [k,v] of map)
Set:
new Set(), set.add(v), set.has(v), set.delete(v), set.clear(), set.size
Iteration: for (const v of set), set.values() (same as keys()), set.entries() yields [v, v] pairs
Important: Map and Set are iterable and work with for...of and spread syntax.
Difference between Map and Object High-level comparison:
Key types:
Object: String (and Symbol) keys only. Non-strings are coerced to strings.
Map: Any value (objects, functions, primitives) can be keys.
Order:
Object: Historically unordered. Modern spec defines property enumeration order (insertion order for non-integer keys, integer keys ordered), but it's nuanced and can be surprising.
Map: Preserves insertion order consistently.
Size:
Object: No built-in size; you must compute Object.keys(obj).length.
Map: map.size is O(1).
Prototypes and collisions:
Object: Has prototype chain — accidental collisions with inherited property names can occur (unless created with Object.create(null)).
Map: No prototype keys; keys are explicit entries.
Performance:
Map is optimized for frequent additions/removals and lookups with arbitrary keys. Objects may be fast for plain keyed data but can be less predictable depending on use. Serialization:
Object: JSON.stringify works out of the box.
Map: JSON.stringify on a Map yields {} by default; you must convert to/from an array or object.
Example where Map is better:
Using objects as keys (e.g., caching results per object): const cache = new Map(); function expensive(obj) { if (cache.has(obj)) return cache.get(obj); const result = /* compute from obj */; cache.set(obj, result); return result; }
Difference between Set and Array High-level comparison:
Uniqueness:
Array: Allows duplicates.
Set: Automatically enforces uniqueness.
Indexing:
Array: Ordered, index-based (arr[0], arr[1]).
Set: Ordered by insertion but no numeric indices.
Methods:
Array: Rich API (map, filter, reduce, slice, etc).
Set: Smaller API; you can convert to an Array to use array methods: [...set]
Membership check:
Array: arr.includes(value) or indexOf -> O(n)
Set: set.has(value) -> O(1) average
Use cases:
Array: Ordered collections where duplicates or indices matter.
Set: Collections where uniqueness and fast membership matter.
Deduplication example:
const arr = [1, 2, 2, 3, 3, 3]; const unique = [...new Set(arr)]; // [1, 2, 3]
Problems with traditional Objects and Arrays Objects:
Only string/symbol keys. Non-string keys are coerced to strings:
obj[{}] becomes obj['[object Object]'] — accidental collisions. Prototype pollution and inherited properties:
obj.hasOwnProperty('toString') might be false in edge cases or accidentally shadowed. No built-in .size (costly to compute).
Iteration order historically unclear; integer-like keys behave specially.
Not ideal for using objects/functions as keys.
Arrays:
Membership checks are O(n) (indexOf/includes), not ideal for large datasets.
Duplicates can be a problem when you want uniqueness.
Arrays can be sparse; length can be misleading.
Frequent insertions/removals in middle of arrays can be costly.
Map and Set address many of these issues:
Map: arbitrary keys, reliable ordering, built-in size.
Set: enforced uniqueness and faster membership testing.
Show uniqueness property of Set Example: Removing duplicates from an array:
const emails = [ 'alice@example.com', 'bob@example.com', 'alice@example.com', 'carol@example.com' ];
const uniqueEmails = [...new Set(emails)]; console.log(uniqueEmails); // ['alice@example.com', 'bob@example.com', 'carol@example.com']
SameValueZero behavior (NaN example):
const s = new Set(); s.add(NaN); s.add(NaN); console.log(s.size); // 1 (NaN treated as equal to NaN in Set)
Comparing Map with key-value storage Think of Map as a general key-value dictionary with these advantages:
Arbitrary key types (not just strings).
Predictable iteration order.
Efficient size lookup and fast lookups.
No prototype interference.
Common patterns where Map shines:
Caching/memoization keyed by object references.
Graph algorithms where nodes (objects) are keys and values store metadata.
Counting occurrences:
const counts = new Map(); for (const item of arr) { counts.set(item, (counts.get(item) || 0) + 1); }
Note on serialization:
Convert Map to array before serializing:
JSON: JSON.stringify([...map]) or convert to object with Object.fromEntries(map) Note on WeakMap:
If you need keys that are objects and want them garbage-collected when no longer referenced elsewhere, consider WeakMap. WeakMap keys must be objects and are held weakly for GC. When to use Map and Set Use Map when:
You need keys that aren’t strings (objects, functions, numbers).
You want a reliable insertion-order traversal of entries.
You need an efficient size property.
You want to avoid prototype key collisions.
Use Set when:
You need to enforce uniqueness.
You need fast membership checks (has).
You want to easily dedupe lists.
You represent collections of items where indexing is not needed.
When to stick with Object/Array:
Simple JSON data you need to serialize directly (use objects/arrays).
You rely on array methods (map/filter/reduce) and indexing.
Performance characteristics of plain objects are required and keys are always string-like.
Few Practical examples (real-world) Tracking unique visitors (Set)
const visitors = new Set(); function recordVisit(userId) { visitors.add(userId); } recordVisit('user123'); recordVisit('user456'); recordVisit('user123'); // duplicate, no effect console.log(visitors.size); // 2
Caching computations for objects (Map)
const resultCache = new Map();
function compute(obj) { if (resultCache.has(obj)) { return resultCache.get(obj); } const res = /* expensive computation using obj */; resultCache.set(obj, res); return res; }
Counting words or items (Map)
function wordCount(words) { const counts = new Map(); for (const w of words) { counts.set(w, (counts.get(w) || 0) + 1); } return counts; }
Implementing adjacency list for a graph using Map
const graph = new Map(); // node -> Set(neighbors) function addEdge(a, b) { if (!graph.has(a)) graph.set(a, new Set()); graph.get(a).add(b); }
Diagram ideas Map key-value storage visual (ASCII):
Map (insertion order) +---------------------------------+ | [key: 'name'] -> 'Alice' | | [key: 42] -> 'the answer' | | [key: {id:1}] -> 'object val' | +---------------------------------+
Map visualization (Mermaid flowchart)
flowchart LR A("Map") --> B["'name' -> 'Alice'"] A --> C["42 -> 'the answer'"] A --> D["{id:1} -> 'object val'"]
Set uniqueness representation (ASCII):
Set +-----------------------+ | 'alice@example.com' | | 'bob@example.com' | | 'carol@example.com' | +-----------------------+
Array with duplicates: ['alice@example.com', 'bob@example.com', 'alice@example.com'] | v Dedup -> [...new Set(array)] -> ['alice@example.com', 'bob@example.com']
Set visualization (Mermaid)
graph TD A["Array: [a, b, a, c]"] --> B["new Set(...)"] B --> C["Set: {a, b, c}"] C --> D["[...Set] => [a, b, c]"]
Conclusion Think of Objects and Arrays as your everyday tools—reliable and simple. But when you need performance, unique lists, or complex keys, reach for Map and Set.
Use Map when you need a dictionary with non-string keys or need to know the size easily.
Use Set when you need a list of items and you want to ensure there are zero duplicates.
They complement (not always replace) Objects and Arrays. Objects and Arrays remain ideal for JSON data and when you need array utilities or simple literal structures. For caches, graph structures, unique collections, and situations where keys are non-strings, prefer Map and Set.
