JavaScript Map and Set: The Data Structures You Should Be Using Instead of Objects and Arrays

JavaScript Map and Set: The Data Structures You Should Be Using Instead of Objects and Arrays
Audience: This post assumes working knowledge of JavaScript, including ES6+ syntax and familiarity with Objects and Arrays.
TL;DR: Map gives you a true key-value store where keys can be any type and insertion order is guaranteed. Set gives you a collection of unique values with O(1) lookup. Both outperform Object and Array in specific scenarios — knowing when to use them makes your code faster and more correct.
Problem
JavaScript developers default to Objects for key-value storage and Arrays for collections. That works — until it doesn't.
Here are three real problems that Object and Array don't handle cleanly:
1. Object keys are always strings (or Symbols)
const cache = {};
const keyA = { id: 1 };
const keyB = { id: 2 };
cache[keyA] = 'value A';
cache[keyB] = 'value B';
console.log(cache);
// { '[object Object]': 'value B' }
// Both keys coerced to the same string — data silently overwritten
2. Checking Array uniqueness is O(n)
const visited = [];
function alreadyVisited(url) {
return visited.includes(url); // O(n) scan every time
}
With 100,000 URLs, this becomes a performance problem fast.
3. Object iteration order is unreliable for mixed key types
Integer-like keys get sorted first, then string keys in insertion order. This surprises many developers and causes subtle bugs in ordered data processing.
Solution
What is Map?
Map is a key-value data structure where keys can be any value — objects, functions, primitives — and insertion order is always preserved.
Map Internal Structure (conceptual):
┌─────────────────────────────────────────┐
│ MAP │
├──────────────────┬──────────────────────┤
│ KEY │ VALUE │
├──────────────────┼──────────────────────┤
│ {id: 1} (obj) │ 'User Profile Data' │
│ 42 (number) │ 'Answer' │
│ 'name' (str) │ 'Alice' │
│ fn() (func) │ 'Handler Result' │
└──────────────────┴──────────────────────┘
Keys preserve insertion order
Keys are compared by identity/value, not toString()
const userCache = new Map();
const userObject = { id: 1 };
userCache.set(userObject, { name: 'Alice', role: 'admin' });
userCache.set(42, 'some numeric key value');
userCache.set('status', 'active');
console.log(userCache.get(userObject)); // { name: 'Alice', role: 'admin' }
console.log(userCache.get(42)); // 'some numeric key value'
console.log(userCache.size); // 3
Core Map API:
const inventory = new Map();
// Set entries
inventory.set('apples', 50);
inventory.set('bananas', 30);
inventory.set('cherries', 120);
// Get a value
console.log(inventory.get('apples')); // 50
// Check existence
console.log(inventory.has('bananas')); // true
console.log(inventory.has('grapes')); // false
// Delete an entry
inventory.delete('bananas');
console.log(inventory.size); // 2
// Iterate in insertion order — guaranteed
for (const [item, count] of inventory) {
console.log(`${item}: ${count}`);
}
// apples: 50
// cherries: 120
// Convert to array of pairs
console.log([...inventory.entries()]);
// [['apples', 50], ['cherries', 120]]
What is Set?
Set is a collection of unique values. Adding a duplicate does nothing — it doesn't throw, it just ignores the duplicate. Lookups are O(1) because Set uses a hash-based structure internally.
Set Internal Structure (conceptual):
Input: [1, 2, 2, 3, 3, 3, 4]
│
▼
┌─────────────────────┐
│ SET │
│ ┌───┬───┬───┬───┐ │
│ │ 1 │ 2 │ 3 │ 4 │ │
│ └───┴───┴───┴───┘ │
│ Duplicates removed │
│ Insertion order │
│ preserved │
└─────────────────────┘
const tagSet = new Set();
tagSet.add('javascript');
tagSet.add('nodejs');
tagSet.add('javascript'); // duplicate — silently ignored
tagSet.add('performance');
console.log(tagSet.size); // 3
console.log(tagSet.has('nodejs')); // true
console.log(tagSet.has('python')); // false
// Iterate
for (const tag of tagSet) {
console.log(tag);
}
// javascript
// nodejs
// performance
Practical deduplication pattern:
const rawPageViews = [
'homepage', 'about', 'homepage', 'pricing',
'about', 'homepage', 'contact'
];
// Deduplicate in one line
const uniquePages = [...new Set(rawPageViews)];
console.log(uniquePages);
// ['homepage', 'about', 'pricing', 'contact']
console.log(`${rawPageViews.length} views across ${uniquePages.length} unique pages`);
// 7 views across 4 unique pages
Map vs Object: The Real Differences
| Concern | Object | Map |
| Key types | String or Symbol only | Any value (objects, functions, primitives) |
| Key ordering | Integers first, then insertion | Always insertion order |
| Size | Manual (Object.keys(obj).length) | map.size directly |
| Prototype pollution | Yes — inherits keys like toString | No prototype, clean by default |
| Iteration | for...in (includes inherited), Object.keys() | for...of directly, clean |
| Performance (frequent add/delete) | Slower | Optimized for this use case |
| JSON serialization | Native JSON.stringify | Requires manual conversion |
Prototype pollution is a real footgun with Object:
const config = {};
console.log(config['toString']); // [Function: toString] — unexpected!
console.log(config['constructor']); // [Function: Object] — unexpected!
// With Map, this doesn't happen
const configMap = new Map();
console.log(configMap.get('toString')); // undefined — clean
When Object wins over Map:
// Object is better for static, known-shape records
const user = {
id: 42,
name: 'Alice',
email: 'alice@example.com'
};
// JSON serialization is trivial with Object
const json = JSON.stringify(user);
// Map would need: JSON.stringify([...myMap.entries()])
Set vs Array: The Real Differences
| Concern | Array | Set |
| Duplicates | Allowed | Not allowed |
Lookup (includes / has) | O(n) linear scan | O(1) hash lookup |
| Index access | arr[0], arr[2] | Not supported |
| Order | Insertion order | Insertion order |
| Use case | Ordered list, index access | Unique collection, membership test |
Performance benchmark — membership testing:
// Build a large dataset
const size = 100_000;
const dataArray = Array.from({ length: size }, (_, i) => i);
const dataSet = new Set(dataArray);
const target = 99_999; // worst case for array (near end)
// Array lookup
console.time('Array.includes');
for (let i = 0; i < 1000; i++) {
dataArray.includes(target);
}
console.timeEnd('Array.includes');
// Array.includes: ~45ms
// Set lookup
console.time('Set.has');
for (let i = 0; i < 1000; i++) {
dataSet.has(target);
}
console.timeEnd('Set.has');
// Set.has: ~0.1ms
// ~450x faster for large collections
Set math operations (union, intersection, difference):
const premiumUsers = new Set([101, 102, 103, 104]);
const activeUsers = new Set([102, 103, 105, 106]);
// Intersection — users who are both premium AND active
const premiumAndActive = new Set(
[...premiumUsers].filter(id => activeUsers.has(id))
);
console.log([...premiumAndActive]); // [102, 103]
// Union — all relevant users
const allRelevant = new Set([...premiumUsers, ...activeUsers]);
console.log([...allRelevant]); // [101, 102, 103, 104, 105, 106]
// Difference — premium users who are NOT active
const premiumNotActive = new Set(
[...premiumUsers].filter(id => !activeUsers.has(id))
);
console.log([...premiumNotActive]); // [101, 104]
When to Use Map
Use Map when:
- Keys are not strings (DOM nodes, objects, numbers as meaningful keys)
- You need guaranteed insertion-order iteration
- You're frequently adding and deleting keys (Map is optimized for this)
- You want to avoid prototype key collisions
- You need
sizewithout computing it
// Good Map use case: DOM node metadata
const elementMetadata = new Map();
const button = document.querySelector('#submit-btn');
const input = document.querySelector('#email-input');
elementMetadata.set(button, { clickCount: 0, lastClicked: null });
elementMetadata.set(input, { validationErrors: [], dirty: false });
button.addEventListener('click', () => {
const meta = elementMetadata.get(button);
meta.clickCount++;
meta.lastClicked = Date.now();
});
// The DOM node itself is the key — impossible cleanly with Object
When to Use Set
Use Set when:
- You need uniqueness enforced automatically
- You're doing membership checks repeatedly on a large collection
- You need set math (union, intersection, difference)
- You want to deduplicate an array
// Good Set use case: tracking processed job IDs
const processedJobs = new Set();
async function processJob(jobId, payload) {
if (processedJobs.has(jobId)) {
console.log(`Job ${jobId} already processed — skipping`);
return;
}
// Process the job...
await runJob(payload);
processedJobs.add(jobId);
console.log(`Job ${jobId} completed`);
}
Results
- Set.has vs Array.includes on 100,000 items: ~450x faster in testing (0.1ms vs ~45ms per 1,000 lookups)
- Map eliminates prototype pollution bugs that silently corrupt Object-based key stores
- Set deduplication is a single-line operation with no manual tracking
- Both structures have
O(1)average time complexity for get/set/has/delete operations
Trade-offs
Map limitations:
- JSON serialization is not built-in.
JSON.stringify(myMap)returns{}. You must convert:JSON.stringify([...myMap]) - Slightly more verbose to construct than an object literal
- Not suitable when you need a fixed-shape record (use Object for that)
Set limitations:
- No index access. If you need
collection[3], use Array - Object uniqueness is by reference, not by value. Two objects with identical contents are treated as different entries
// Set reference equality gotcha
const s = new Set();
s.add({ name: 'Alice' });
s.add({ name: 'Alice' }); // Different reference — both added!
console.log(s.size); // 2 — not 1
// To handle this, you'd need to serialize or use a unique key strategy
Conclusion
Map and Set are not exotic additions to JavaScript — they are the right tool for specific, common problems. Use Map when you need a true key-value store with any key type or guaranteed ordering. Use Set when you need unique values and fast membership tests. Default to Object and Array only when their specific traits (JSON serialization, index access, known shape) are what you actually need.
The next step: audit existing code where you're using Array.includes() in a hot path, or using an Object where keys might collide with inherited properties. Those are immediate candidates for replacement.
Further Reading
- MDN: Map
- MDN: Set
- V8 Blog: Fast Properties in V8 — explains why Object key handling is complex internally
- ECMAScript Spec: Map and Set — if you want to understand the spec-level guarantees
- JavaScript.info: Map and Set — concise reference with good examples



