JavaScript Map and Set
Map and Set are built-in JavaScript collection types introduced in ES6. A Map stores key-value pairs where keys can be of any type, and a Set stores a collection of unique values. They solve problems that plain objects and arrays handle awkwardly: objects only allow string or symbol keys and carry inherited properties, while arrays require manual work to keep values unique. Understanding Map and Set well means understanding not just their methods, but how they compare values internally and how they preserve insertion order.
Overview: How Map and Set Work
A Map is an ordered collection of key-value pairs. Unlike a plain object, a Map can use any value as a key — not just strings and symbols. You can use numbers, booleans, functions, objects, or even other Maps as keys. A Set is an ordered collection of unique values, where each value may occur only once, similar to the mathematical concept of a set.
Both structures use an internal algorithm called SameValueZero to decide whether two values are ‘the same’. This is almost identical to strict equality (===), with one important exception: NaN is considered equal to itself under SameValueZero, even though NaN === NaN is false everywhere else in JavaScript. This means a Set can only ever contain one NaN, and a Map can only ever have one entry keyed by NaN.
Internally, engines implement Map and Set using hash tables, similar to how objects store properties, but without the baggage of a prototype chain or the string/symbol key restriction. Because they are hash-table based, get, set, has, and delete all run in average constant time, O(1), regardless of how many entries the collection holds. Both types also remember insertion order: when you iterate a Map or a Set, entries come back in the exact order they were added, which is not guaranteed for all key types in plain objects.
A closely related pair of types, WeakMap and WeakSet, exist for cases where you want object keys that don’t prevent garbage collection. They are covered separately, but it’s worth knowing they exist: a regular Map holds a strong reference to every key, so objects used as keys stay in memory as long as the Map does.
Syntax
const map = new Map(iterable);
const set = new Set(iterable);
The optional iterable argument seeds the collection: for Map it must be an iterable of [key, value] pairs (such as an array of two-element arrays, or another Map); for Set it can be any iterable of values (an array, a string, another Set, and so on).
| Map method / property | What it does |
|---|---|
set(key, value) |
Adds or updates an entry; returns the map, so calls can be chained |
get(key) |
Returns the value for key, or undefined if it isn’t present |
has(key) |
Returns true if the key exists |
delete(key) |
Removes the entry; returns true if something was removed |
clear() |
Removes every entry |
size |
A property (not a method) holding the number of entries |
keys() / values() / entries() |
Return iterators over keys, values, or [key, value] pairs |
| Set method / property | What it does |
|---|---|
add(value) |
Adds a value; returns the set, so calls can be chained |
has(value) |
Returns true if the value exists |
delete(value) |
Removes the value; returns true if something was removed |
clear() |
Removes every value |
size |
A property holding the number of values |
Examples
Example 1: Basic Map usage
const user = new Map();
user.set('name', 'Ava');
user.set('age', 29);
user.set(true, 'admin');
console.log(user.get('name'));
console.log(user.get(true));
console.log(user.size);
for (const [key, value] of user) {
console.log(`${key} -> ${value}`);
}
Ava
admin
3
name -> Ava
age -> 29
true -> admin
Notice that the key true is a boolean, not the string 'true' — something a plain object could never do, since object keys are always coerced to strings. Iterating with for...of yields [key, value] pairs in the exact order they were inserted.
Example 2: Basic Set usage
const numbers = [1, 2, 2, 3, 3, 3, 4];
const uniqueNumbers = new Set(numbers);
console.log(uniqueNumbers.size);
console.log([...uniqueNumbers]);
uniqueNumbers.add(10);
uniqueNumbers.delete(1);
console.log(uniqueNumbers.has(2));
console.log(uniqueNumbers.has(1));
for (const value of uniqueNumbers) {
console.log(value);
}
4
[ 1, 2, 3, 4 ]
true
false
2
3
4
10
Passing the array straight into new Set() is the idiomatic way to deduplicate a list. Spreading a set back into an array with [...uniqueNumbers] gives you a plain array again, still in insertion order.
Example 3: A realistic use case
function wordFrequency(text) {
const counts = new Map();
const words = text.toLowerCase().match(/[a-z']+/g);
for (const word of words) {
counts.set(word, (counts.get(word) || 0) + 1);
}
return counts;
}
const text = 'the quick fox jumps over the lazy dog the fox runs';
const freq = wordFrequency(text);
console.log(freq.size);
console.log(freq.get('the'));
console.log(freq.get('fox'));
console.log(freq.get('dog'));
const tags = new Set();
const posts = [
{ title: 'Intro to JS', tags: ['js', 'beginner'] },
{ title: 'Advanced JS', tags: ['js', 'advanced'] },
{ title: 'CSS Grid', tags: ['css', 'beginner'] },
];
for (const post of posts) {
for (const tag of post.tags) {
tags.add(tag);
}
}
console.log(tags.size);
console.log([...tags]);
8
3
2
1
4
[ 'js', 'beginner', 'advanced', 'css' ]
This combines both structures the way real code often does: a Map accumulates counts keyed by word (the counts.get(word) || 0 pattern is a common way to provide a default of zero for a first-time key), while a Set collects the unique tags across every post, automatically discarding duplicates like the repeated 'js' and 'beginner' tags.
Under the Hood: Reference Keys and SameValueZero
const cache = new Map();
const keyObj1 = { id: 1 };
const keyObj2 = { id: 1 };
cache.set(keyObj1, 'first');
cache.set(keyObj2, 'second');
console.log(cache.size);
console.log(cache.get(keyObj1));
console.log(cache.get(keyObj2));
console.log(cache.get({ id: 1 }));
const seen = new Set();
seen.add(NaN);
seen.add(NaN);
console.log(seen.size);
console.log(seen.has(NaN));
2
first
second
undefined
1
true
Even though keyObj1 and keyObj2 look structurally identical, they are two different object references, so the Map treats them as two separate keys — that’s why size is 2. Looking the value up with a brand-new object literal { id: 1 } fails and returns undefined, because that object is yet another reference the Map has never seen. This is a direct consequence of SameValueZero comparing objects by identity, not by shape. The NaN case shows the opposite side of SameValueZero: two additions of NaN collapse into a single entry, because SameValueZero (unlike ===) treats NaN as equal to itself.
Common Mistakes
Mistake 1: Using bracket notation instead of set/get
Because Map instances are objects, JavaScript lets you assign arbitrary properties to them with bracket notation. This compiles fine but silently does the wrong thing — it sets a normal object property, not a Map entry.
const scores = new Map();
scores['alice'] = 90;
scores.set('bob', 85);
console.log(scores.size);
console.log(scores.get('alice'));
console.log(scores['alice']);
1
undefined
90
scores.size is only 1 because 'alice' was never added as a Map entry; it became a plain JavaScript property sitting alongside the Map’s internal data. The fix is to always use set and get:
const scores = new Map();
scores.set('alice', 90);
scores.set('bob', 85);
console.log(scores.size);
console.log(scores.get('alice'));
2
90
Mistake 2: Reading .length instead of .size
Arrays and strings use .length, but Map and Set use .size. Mixing them up is one of the most common slips, and it fails silently because .length simply doesn’t exist on these types.
const items = new Set(['a', 'b', 'c']);
console.log(items.length);
console.log(items.size);
undefined
3
Since Map and Set are not array-like, there is no .length property at all — the expression simply evaluates to undefined rather than throwing an error, which makes the bug easy to miss in larger programs.
Best Practices
- Reach for a
Mapwhen keys aren’t simple strings, when key order matters, or when you need to add and remove keys frequently — plain objects were never designed for that churn. - Reach for a
Setwhenever you need a de-duplicated collection or want fasthas()membership checks instead of scanning an array withincludes(). - Convert a
MaporSetback to an array with the spread operator ([...map.entries()],[...set]) whenever you need array methods likemap,filter, orsort. - Use
Object.fromEntries(map)to convert aMapinto a plain object when you need to serialize it withJSON.stringify, sinceJSON.stringifydoes not supportMaporSetdirectly. - Prefer
new Set(array)over manual loops for deduplication — it is both shorter and clearer about intent. - When using objects as Map keys, remember that equality is by reference; if you need value-based key matching, serialize the key (for example with
JSON.stringify) or use a string/number derived from its contents instead. - Consider
WeakMaporWeakSetinstead ofMaporSetwhen the keys are objects that should be garbage-collected once nothing else references them, such as metadata attached to DOM nodes.
Practice Exercises
- Write a function
countChars(str)that returns aMapwhere each key is a character fromstrand each value is how many times that character appears. - Given two arrays, write a function
intersection(a, b)that uses twoSetinstances to return an array of values present in both arrays, with no duplicates. - Write a function
firstRepeatedValue(array)that uses aSetto find and return the first value inarraythat appears more than once, orundefinedif there is none.
Summary
Mapstores ordered key-value pairs where keys can be any type;Setstores an ordered collection of unique values.- Both use SameValueZero equality: it behaves like
===except it treatsNaNas equal to itself, so a collection can only hold oneNaN. - Object keys in a
Mapare compared by reference, not by structural content. sizeis a property that reports how many entries or values exist — not.length, and not a method call.- Both types preserve insertion order during iteration, unlike some cases with plain object keys.
- Use
set/get/has/deleteforMap, andadd/has/deleteforSet— never bracket notation. WeakMapandWeakSetexist as garbage-collection-friendly alternatives when keys are objects.
