JavaScript Map and Set: Collection Types

JavaScript developers typically reach for objects when storing key-value pairs and arrays for ordered collections. But objects have quirks: keys are always strings or symbols, property enumeration...

Key Insights

  • Map and Set provide true hash-based collections with predictable iteration order and better performance for frequent additions/deletions compared to plain objects and arrays
  • Maps allow any data type as keys (objects, functions, primitives), while objects coerce keys to strings, making Maps essential for complex key-value relationships
  • WeakMap and WeakSet enable memory-efficient caching and metadata storage by allowing garbage collection of unused references without manual cleanup

Beyond Arrays and Objects

JavaScript developers typically reach for objects when storing key-value pairs and arrays for ordered collections. But objects have quirks: keys are always strings or symbols, property enumeration can be unpredictable, and distinguishing between your data and inherited properties requires extra checks. Arrays work well for indexed collections but struggle with membership testing and deduplication.

Map and Set solve these problems. Map provides a true hash map with any data type as keys, predictable iteration order, and a clean API. Set offers an efficient collection of unique values with O(1) membership testing. Both outperform their traditional counterparts for specific operations and make your intent clearer.

Here’s a concrete example of where objects fall short:

// Using an object - keys get stringified
const userRoles = {};
const user1 = { id: 1, name: 'Alice' };
const user2 = { id: 2, name: 'Bob' };

userRoles[user1] = 'admin';
userRoles[user2] = 'editor';

console.log(userRoles); 
// { '[object Object]': 'editor' } - both keys became the same string!

// Using Map - objects are proper keys
const userRolesMap = new Map();
userRolesMap.set(user1, 'admin');
userRolesMap.set(user2, 'editor');

console.log(userRolesMap.get(user1)); // 'admin'
console.log(userRolesMap.get(user2)); // 'editor'
console.log(userRolesMap.size); // 2

Map: Key-Value Pairs Done Right

Map is a collection of keyed data items, similar to objects, but with crucial differences. You can use any value as a key—objects, functions, primitives—and Map maintains insertion order. The API is straightforward: set() to add, get() to retrieve, has() to check existence, and delete() to remove.

Create Maps from scratch or initialize them with an iterable of key-value pairs:

// Empty Map
const cache = new Map();

// Initialize with array of [key, value] pairs
const settings = new Map([
  ['theme', 'dark'],
  ['language', 'en'],
  ['notifications', true]
]);

// Add and retrieve values
cache.set('user:123', { name: 'Alice', age: 30 });
cache.set('user:456', { name: 'Bob', age: 25 });

console.log(cache.get('user:123')); // { name: 'Alice', age: 30 }
console.log(cache.has('user:789')); // false
console.log(cache.size); // 2

The real power shows when using non-primitive keys. This enables patterns impossible with plain objects:

// Using functions as keys
const functionMetadata = new Map();
const handleClick = () => console.log('clicked');
const handleSubmit = () => console.log('submitted');

functionMetadata.set(handleClick, { calls: 0, lastCalled: null });
functionMetadata.set(handleSubmit, { calls: 0, lastCalled: null });

// Using DOM elements as keys
const elementData = new Map();
const button = document.querySelector('button');
elementData.set(button, { clickCount: 0, enabled: true });

Map provides multiple iteration methods. Choose based on what you need:

const scores = new Map([
  ['Alice', 95],
  ['Bob', 87],
  ['Charlie', 92]
]);

// forEach with callback
scores.forEach((score, name) => {
  console.log(`${name}: ${score}`);
});

// for...of with destructuring
for (const [name, score] of scores) {
  console.log(`${name} scored ${score}`);
}

// Iterate over keys only
for (const name of scores.keys()) {
  console.log(name);
}

// Iterate over values only
for (const score of scores.values()) {
  console.log(score);
}

// Get entries as array
const entries = [...scores.entries()];

Set: Unique Values Collection

Set stores unique values of any type. Attempting to add a duplicate does nothing—Set automatically handles deduplication. This makes Set perfect for removing duplicates, tracking unique items, and membership testing.

The most common use case is deduplication:

const numbers = [1, 2, 2, 3, 4, 4, 5, 1];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

// Works with objects too (by reference)
const tags = [
  { id: 1, name: 'javascript' },
  { id: 2, name: 'react' },
  { id: 1, name: 'javascript' }
];
const uniqueTags = [...new Set(tags)];
console.log(uniqueTags.length); // 3 - objects are different references

Set operations are straightforward:

const skills = new Set();

// Add values
skills.add('JavaScript');
skills.add('Python');
skills.add('JavaScript'); // Ignored - already exists

// Check membership - O(1) operation
console.log(skills.has('JavaScript')); // true
console.log(skills.has('Ruby')); // false

// Remove values
skills.delete('Python');
console.log(skills.size); // 1

// Clear all values
skills.clear();

Implement mathematical set operations using spread and array methods:

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// Union - all elements from both sets
const union = new Set([...setA, ...setB]);
console.log([...union]); // [1, 2, 3, 4, 5, 6]

// Intersection - elements in both sets
const intersection = new Set(
  [...setA].filter(x => setB.has(x))
);
console.log([...intersection]); // [3, 4]

// Difference - elements in A but not in B
const difference = new Set(
  [...setA].filter(x => !setB.has(x))
);
console.log([...difference]); // [1, 2]

WeakMap and WeakSet: Memory-Efficient Variants

WeakMap and WeakSet hold “weak” references to objects, allowing garbage collection when no other references exist. This prevents memory leaks in scenarios where you’re attaching metadata to objects you don’t control.

WeakMap only accepts objects as keys and doesn’t prevent those objects from being garbage collected. You can’t iterate over WeakMap or check its size—these limitations are necessary for the garbage collection behavior.

Use WeakMap for private data or caching without memory leaks:

// Store private data keyed by object instances
const privateData = new WeakMap();

class User {
  constructor(name) {
    // Store private data
    privateData.set(this, { 
      passwordHash: this.hashPassword(name),
      loginAttempts: 0 
    });
  }
  
  hashPassword(password) {
    // Simplified for example
    return `hashed_${password}`;
  }
  
  getLoginAttempts() {
    return privateData.get(this).loginAttempts;
  }
  
  incrementLoginAttempts() {
    const data = privateData.get(this);
    data.loginAttempts++;
  }
}

// Cache metadata about DOM elements
const elementMetadata = new WeakMap();

function attachMetadata(element, data) {
  elementMetadata.set(element, data);
}

function getMetadata(element) {
  return elementMetadata.get(element);
}

// When the DOM element is removed and no other references exist,
// the metadata is automatically garbage collected

WeakSet works similarly but stores unique objects without preventing garbage collection:

const clickedElements = new WeakSet();

document.addEventListener('click', (e) => {
  if (clickedElements.has(e.target)) {
    console.log('Already clicked');
  } else {
    clickedElements.add(e.target);
    console.log('First click');
  }
});

Performance and Use Cases

Map and Set offer performance advantages for specific operations. Map provides O(1) average-case lookup, insertion, and deletion—significantly faster than searching through object properties or array elements for large collections.

Here’s a practical caching example:

// Inefficient array-based cache
class ArrayCache {
  constructor() {
    this.cache = [];
  }
  
  set(key, value) {
    const index = this.cache.findIndex(item => item.key === key);
    if (index >= 0) {
      this.cache[index].value = value;
    } else {
      this.cache.push({ key, value });
    }
  }
  
  get(key) {
    const item = this.cache.find(item => item.key === key); // O(n)
    return item ? item.value : undefined;
  }
}

// Efficient Map-based cache
class MapCache {
  constructor() {
    this.cache = new Map();
  }
  
  set(key, value) {
    this.cache.set(key, value); // O(1)
  }
  
  get(key) {
    return this.cache.get(key); // O(1)
  }
}

// Performance comparison
const arrayCache = new ArrayCache();
const mapCache = new MapCache();

console.time('Array Cache');
for (let i = 0; i < 10000; i++) {
  arrayCache.set(`key${i}`, i);
}
for (let i = 0; i < 10000; i++) {
  arrayCache.get(`key${i}`);
}
console.timeEnd('Array Cache'); // Significantly slower

console.time('Map Cache');
for (let i = 0; i < 10000; i++) {
  mapCache.set(`key${i}`, i);
}
for (let i = 0; i < 10000; i++) {
  mapCache.get(`key${i}`);
}
console.timeEnd('Map Cache'); // Much faster

Common Patterns and Best Practices

Converting between Maps, objects, and arrays is straightforward:

// Object to Map
const obj = { a: 1, b: 2, c: 3 };
const mapFromObj = new Map(Object.entries(obj));

// Map to Object
const map = new Map([['x', 10], ['y', 20]]);
const objFromMap = Object.fromEntries(map);

// Array to Set and back
const arr = [1, 2, 3, 2, 1];
const set = new Set(arr);
const uniqueArr = [...set];

Use Map for frequency counting—a common pattern that’s cleaner than objects:

function countOccurrences(items) {
  const counts = new Map();
  
  for (const item of items) {
    counts.set(item, (counts.get(item) || 0) + 1);
  }
  
  return counts;
}

const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];
const wordCounts = countOccurrences(words);

for (const [word, count] of wordCounts) {
  console.log(`${word}: ${count}`);
}
// apple: 3
// banana: 2
// cherry: 1

Use Set for efficient filtering and deduplication:

// Remove items that appear in exclusion list
function filterExcluded(items, excluded) {
  const excludedSet = new Set(excluded);
  return items.filter(item => !excludedSet.has(item));
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const toExclude = [2, 4, 6, 8];
console.log(filterExcluded(numbers, toExclude)); // [1, 3, 5, 7, 9]

// Find common elements between arrays
function findCommon(arr1, arr2) {
  const set2 = new Set(arr2);
  return [...new Set(arr1.filter(item => set2.has(item)))];
}

Choose the right collection type based on your needs: use Map when you need key-value pairs with non-string keys or frequent additions/deletions, Set for unique values and membership testing, WeakMap/WeakSet for object-keyed data that shouldn’t prevent garbage collection, and stick with objects for simple string-keyed data with known properties. Understanding these collection types and their trade-offs makes you a more effective JavaScript developer.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.