Understanding Map in JavaScript (The Underestimated Data Structure)
JavaScript’s Map
is often overshadowed by Object
, but for many key–value use cases it’s the better tool—faster, safer, and more flexible. This article walks through what a Map
is, how it works under the hood (hashing, buckets, collisions), why operations are usually O(1), when they’re not, and when to choose Map
over Object
.
What is a Map
?
In simple terms:
Map
is a key–value data structure (a hash table).
You store a value under a key and later retrieve it using the same key.
Unlike plain objects, Map
allows any type of key (strings, numbers, objects, functions) and gives predictable iteration, a built-in size
, and methods tailored for dictionary behavior.
A Quick Example (and a common mistake)
// ❌ Common mistake: Using Object like a Map
let players = {
USER_1: { name: 'Shubham', score: 120 },
};
// This will throw: players.set is not a function
players.set('USER_2', { name: 'Atharva', score: 200 });
// ✅ Correct: Use Map for key–value storage
const playersMap = new Map();
playersMap.set('USER_1', { name: 'Shubham', score: 120 });
playersMap.set('USER_2', { name: 'Atharva', score: 200 });
console.log(playersMap.get('USER_1')); // { name: 'Shubham', score: 120 }
console.log(playersMap.has('USER_2')); // true
console.log(playersMap.size); // 2
Time Complexity — Is It Always O(1)?
Intuition says “hash maps are O(1), right?” Usually, yes—but the complete answer should include both best and worst cases:
-
Best/Average Case:
O(1)
forget
,set
,has
,delete
-
Worst Case:
O(n)
when many keys collide into the same bucket
Modern JS engines use high-quality hashing + resizing to keep the average case very close to O(1)
in practice.
How Does Map
Work Internally? (Hashing, Buckets, Collisions)
Let’s break it down step by step.
1) Hashing the Key
When you do:
playersMap.set('USER_2', { name: 'Atharva', score: 200 });
- The key
'USER_2'
is passed through a hash function that produces a numeric hash. - Example:
'USER_2'
→ 738492 (pretend hash value).
2) Finding the Bucket
The hash picks which bucket (slot in an internal array) to use:
bucketIndex = hash % tableSize
Example:
738492 % 1000 = 492 → store in bucket 492
Internally, a hash table is like a giant array of buckets.
3) Handling Collisions
Two different keys can produce the same bucket index → a collision.
Common strategies:
- Chaining: store multiple entries in a small list (linked list or array) inside the bucket.
- Open addressing: if a bucket is full, probe to find another empty slot.
4) Retrieving a Value
When you do:
playersMap.get('USER_1');
- Hash
'USER_1'
again. - Jump to the computed bucket.
- Search the bucket’s entries for an exact key match (
===
). - Return the stored value.
-
Average time:
O(1)
-
Worst case:
O(n)
if a bucket’s chain becomes very long
Why Do Collisions Happen?
Because we map infinitely many possible keys to a finite number of buckets. A few key ideas help reason about collisions:
1) Load Factor (α)
α = n / m
-
n
= number of entries -
m
= number of buckets
If α
grows too large, average bucket length grows → more collisions → slower lookups.
Example: Suppose a table has m = 6
buckets and we insert n = 8
entries:
const playersMap = new Map([
['USER_1', { score: 120 }],
['USER_2', { score: 210 }],
['USER_3', { score: 100 }],
['USER_4', { score: 220 }],
['USER_5', { score: 140 }],
['USER_6', { score: 200 }],
['USER_7', { score: 130 }],
['USER_8', { score: 180 }],
]);
Here α = 8 / 6 ≈ 1.33
. By the Pigeonhole Principle, at least one bucket must hold 2+ entries. To keep α healthy, engines resize (rehash) when a threshold is exceeded (commonly around 0.7–0.8).
2) Birthday Paradox Effect
Collisions occur sooner than intuition expects. Just like 23 people suffice for a 50% chance of a shared birthday (with 365 “buckets”), collisions in hash tables also start appearing at surprisingly low n
relative to m
.
How Long Can a Bucket’s “Mini List” Get?
-
Theoretical worst case: all
n
keys land in one bucket → that bucket’s chain length isn
(lookups degrade toO(n)
). -
Expected case: around the load factor
α
per bucket. -
In practice: engines keep
α
low via rehashing. Some languages (like Java’sHashMap
) treeify long chains (e.g., beyond length 8) into balanced trees to cap lookup atO(log n)
. JS engines typically rely on resizing and internal strategies—details are not exposed—but the goal is the same: keep lookups nearO(1)
.
Rehashing (Resizing) — What, Why, When?
What is it?
Rebuilding the table with more buckets and reinserting all existing entries.
Why?
To reduce collisions and maintain near-O(1)
performance as the number of entries grows.
When?
When the load factor threshold is exceeded (e.g., α > ~0.75). This is automatic in JS Map
—you don’t trigger it manually.
How it works (high level):
- Allocate a larger array (often ~2×).
- Recompute
bucketIndex = hash % newTableSize
for each key. - Insert each entry into its new bucket.
- Swap in the new table.
Rehashing is costly in the moment, but it happens infrequently. Spread over many operations, inserts remain amortized O(1).
Map
vs Object
— When to Use Which?
Feature | Map |
Object |
---|---|---|
Key types | Any (string, number, object, function, etc.) | String/Symbol only |
Iteration order | Guaranteed insertion order | Insertion order for string keys is mostly preserved in modern engines, but semantics are object-focused |
Size |
map.size (O(1)) |
Object.keys(obj).length (O(n)) |
Methods |
set , get , has , delete , clear , iterators |
No dedicated map methods; use operators/utilities |
Prototype collisions | None (no prototype keys) | Possible (e.g., toString , unless guarded) |
Performance for dynamic inserts/deletes | Generally better | Not optimized as a hash map |
JSON serialization | Needs conversion (e.g., Object.fromEntries(map) ) |
Natural with JSON.stringify(obj)
|
Guideline:
- Use
Map
for dynamic key–value stores where you add/remove/lookup frequently, need non-string keys, iteration order, orsize
. - Use
Object
for structured data models meant to be serialized to JSON or when working with APIs expecting plain objects.
Extra Tips & Gotchas
- Keys are case-sensitive:
'USER_1'
and'user_1'
are different. - In
Map
, the keys1
and'1'
are different (type matters). - You can use objects as keys:
const m = new Map();
const user = { id: 'USER_1' };
m.set(user, { score: 120 });
console.log(m.get(user)); // { score: 120 }
- To JSON-serialize a
Map
, convert it:
const json = JSON.stringify(Object.fromEntries(playersMap));
TL;DR (Summary)
-
Map
is a hash-table-based key–value store with near-O(1)
operations. - Collisions are inevitable (finite buckets, infinite keys), but kept rare via good hashing and rehashing.
- Worst-case lookups are
O(n)
, but amortized performance remains excellent. - Prefer
Map
overObject
for dictionary-like workloads; preferObject
for structured JSON-like data.
Happy mapping! ✨