Skip to main content

Command Palette

Search for a command to run...

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

Updated
8 min read
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         │
  │  ┌───┬───┬───┬───┐  │
  │  │ 1234 │  │
  │  └───┴───┴───┴───┘  │
  │   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

ConcernObjectMap
Key typesString or Symbol onlyAny value (objects, functions, primitives)
Key orderingIntegers first, then insertionAlways insertion order
SizeManual (Object.keys(obj).length)map.size directly
Prototype pollutionYes — inherits keys like toStringNo prototype, clean by default
Iterationfor...in (includes inherited), Object.keys()for...of directly, clean
Performance (frequent add/delete)SlowerOptimized for this use case
JSON serializationNative JSON.stringifyRequires 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

ConcernArraySet
DuplicatesAllowedNot allowed
Lookup (includes / has)O(n) linear scanO(1) hash lookup
Index accessarr[0], arr[2]Not supported
OrderInsertion orderInsertion order
Use caseOrdered list, index accessUnique 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 size without 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

More from this blog