ES6+ Features

JavaScript Map and Set: Modern Collection Types

Master JavaScript's Map and Set data structures. Learn when to use them over objects and arrays, with practical examples and performance comparisons.

By JavaScript Document Team
mapsetcollectionses6data-structures

ES6 introduced Map and Set as new collection types, offering advantages over traditional objects and arrays for certain use cases. This guide explores these powerful data structures and their weak counterparts.

Understanding Map

Map is a collection of key-value pairs where keys can be of any type, unlike objects which only support strings and symbols as keys.

Creating and Using Maps

// Creating a Map
const map1 = new Map();
const map2 = new Map([
  ['key1', 'value1'],
  ['key2', 'value2'],
  [3, 'three'],
  [true, 'boolean key'],
]);

// Setting values
map1.set('name', 'John');
map1.set(42, 'the answer');
map1.set(true, 'boolean value');
map1.set({ id: 1 }, 'object as key');

// Getting values
console.log(map1.get('name')); // 'John'
console.log(map1.get(42)); // 'the answer'
console.log(map1.get(true)); // 'boolean value'

// Checking existence
console.log(map1.has('name')); // true
console.log(map1.has('age')); // false

// Size property
console.log(map1.size); // 4

// Deleting entries
map1.delete(42);
console.log(map1.has(42)); // false

// Clearing all entries
map1.clear();
console.log(map1.size); // 0

Map vs Object

// Key types comparison
const obj = {};
const map = new Map();

// Objects coerce keys to strings
obj[42] = 'number';
obj['42'] = 'string';
console.log(obj[42]); // 'string' (overwritten)

// Maps preserve key types
map.set(42, 'number');
map.set('42', 'string');
console.log(map.get(42)); // 'number'
console.log(map.get('42')); // 'string'

// Object as key
const user = { name: 'John' };

// Object can't use objects as keys
obj[user] = 'user data';
console.log(obj); // { '[object Object]': 'user data' }

// Map can use objects as keys
map.set(user, 'user data');
console.log(map.get(user)); // 'user data'

Iterating Over Maps

const userMap = new Map([
  ['john', { age: 30, city: 'NYC' }],
  ['jane', { age: 25, city: 'LA' }],
  ['bob', { age: 35, city: 'Chicago' }],
]);

// Iterating with for...of
for (const [key, value] of userMap) {
  console.log(`${key}: ${value.age} years old`);
}

// Using forEach
userMap.forEach((value, key) => {
  console.log(`${key} lives in ${value.city}`);
});

// Getting keys, values, and entries
console.log([...userMap.keys()]); // ['john', 'jane', 'bob']
console.log([...userMap.values()]); // [{ age: 30, ... }, ...]
console.log([...userMap.entries()]); // [['john', { age: 30, ... }], ...]

// Converting to array
const entriesArray = Array.from(userMap);
const keysArray = Array.from(userMap.keys());
const valuesArray = Array.from(userMap.values());

Practical Map Examples

// Word frequency counter
function countWords(text) {
  const words = text.toLowerCase().split(/\s+/);
  const frequency = new Map();

  for (const word of words) {
    frequency.set(word, (frequency.get(word) || 0) + 1);
  }

  return frequency;
}

const text = 'the quick brown fox jumps over the lazy dog the fox';
const wordCount = countWords(text);
console.log(wordCount);
// Map { 'the' => 3, 'quick' => 1, 'brown' => 1, ... }

// Cache implementation
class Cache {
  constructor(maxSize = 10) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  get(key) {
    if (!this.cache.has(key)) return undefined;

    // Move to end (LRU)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.maxSize) {
      // Remove oldest (first) entry
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    this.cache.set(key, value);
  }
}

// Grouping data
function groupBy(array, keyFn) {
  const groups = new Map();

  for (const item of array) {
    const key = keyFn(item);
    if (!groups.has(key)) {
      groups.set(key, []);
    }
    groups.get(key).push(item);
  }

  return groups;
}

const people = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 25 },
];

const byAge = groupBy(people, (person) => person.age);
console.log(byAge.get(25)); // [{ name: 'Alice', ... }, { name: 'Charlie', ... }]

Understanding Set

Set is a collection of unique values of any type. It's similar to an array but automatically prevents duplicates.

Creating and Using Sets

// Creating a Set
const set1 = new Set();
const set2 = new Set([1, 2, 3, 4, 5]);
const set3 = new Set('hello'); // Set { 'h', 'e', 'l', 'o' }

// Adding values
set1.add(1);
set1.add(2);
set1.add(2); // Ignored (duplicate)
set1.add('2'); // Added (different type)
console.log(set1); // Set { 1, 2, '2' }

// Checking existence
console.log(set1.has(1)); // true
console.log(set1.has(3)); // false

// Size property
console.log(set1.size); // 3

// Deleting values
set1.delete(2);
console.log(set1.has(2)); // false

// Clearing all values
set1.clear();
console.log(set1.size); // 0

Set Operations

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

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

// Union
const union = new Set([...setA, ...setB]);
console.log(union); // Set { 1, 2, 3, 4, 5, 6 }

// Intersection
const intersection = new Set([...setA].filter((x) => setB.has(x)));
console.log(intersection); // Set { 3, 4 }

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

// Symmetric difference
const symDifference = new Set([
  ...[...setA].filter((x) => !setB.has(x)),
  ...[...setB].filter((x) => !setA.has(x)),
]);
console.log(symDifference); // Set { 1, 2, 5, 6 }

// Is subset
function isSubset(subset, superset) {
  for (const elem of subset) {
    if (!superset.has(elem)) {
      return false;
    }
  }
  return true;
}

const setC = new Set([2, 3]);
console.log(isSubset(setC, setA)); // true

Iterating Over Sets

const fruits = new Set(['apple', 'banana', 'orange']);

// for...of loop
for (const fruit of fruits) {
  console.log(fruit);
}

// forEach method
fruits.forEach((fruit) => {
  console.log(`Fruit: ${fruit}`);
});

// Converting to array
const fruitsArray = [...fruits];
const fruitsArray2 = Array.from(fruits);

// Set maintains insertion order
const ordered = new Set();
ordered.add('third');
ordered.add('first');
ordered.add('second');

console.log([...ordered]); // ['third', 'first', 'second']

Practical Set Examples

// Tracking unique visitors
class UniqueVisitorTracker {
  constructor() {
    this.visitors = new Set();
  }

  addVisitor(userId) {
    const sizeBefore = this.visitors.size;
    this.visitors.add(userId);
    return this.visitors.size > sizeBefore; // Returns true if new visitor
  }

  getUniqueCount() {
    return this.visitors.size;
  }

  hasVisited(userId) {
    return this.visitors.has(userId);
  }
}

// Remove duplicate objects
const users = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' },
  { id: 1, name: 'John' },
  { id: 3, name: 'Bob' },
];

const uniqueUsers = Array.from(
  new Set(users.map((u) => JSON.stringify(u)))
).map((s) => JSON.parse(s));

// Or using Map for better performance
const userMap = new Map(users.map((u) => [u.id, u]));
const uniqueUsersById = [...userMap.values()];

// Tag system
class TagManager {
  constructor() {
    this.tags = new Set();
  }

  addTags(...newTags) {
    newTags.forEach((tag) => this.tags.add(tag.toLowerCase()));
  }

  removeTags(...tagsToRemove) {
    tagsToRemove.forEach((tag) => this.tags.delete(tag.toLowerCase()));
  }

  hasTag(tag) {
    return this.tags.has(tag.toLowerCase());
  }

  hasAllTags(...tagsToCheck) {
    return tagsToCheck.every((tag) => this.hasTag(tag));
  }

  hasAnyTag(...tagsToCheck) {
    return tagsToCheck.some((tag) => this.hasTag(tag));
  }
}

WeakMap

WeakMap is similar to Map but with key differences:

  • Keys must be objects (not primitives)
  • Keys are weakly held (can be garbage collected)
  • Not iterable
  • No size property

WeakMap Usage

// Creating WeakMap
const wm = new WeakMap();

// Keys must be objects
const obj1 = { id: 1 };
const obj2 = { id: 2 };

wm.set(obj1, 'data for obj1');
wm.set(obj2, 'data for obj2');

console.log(wm.get(obj1)); // 'data for obj1'
console.log(wm.has(obj2)); // true

// Primitive keys not allowed
// wm.set('string', 'value');   // TypeError

// Garbage collection example
let user = { name: 'John' };
const userMeta = new WeakMap();
userMeta.set(user, { lastVisit: Date.now() });

user = null; // Original object can be garbage collected
// The WeakMap entry is also removed automatically

WeakMap Practical Examples

// Private data for objects
const privateData = new WeakMap();

class User {
  constructor(name, email) {
    this.name = name;
    // Store private data
    privateData.set(this, { email });
  }

  getEmail() {
    return privateData.get(this).email;
  }

  setEmail(email) {
    privateData.get(this).email = email;
  }
}

const user = new User('John', 'john@example.com');
console.log(user.name); // 'John'
console.log(user.email); // undefined
console.log(user.getEmail()); // 'john@example.com'

// DOM node metadata
const nodeData = new WeakMap();

function attachMetadata(node, data) {
  nodeData.set(node, data);
}

function getMetadata(node) {
  return nodeData.get(node);
}

// Event listener management
class EventManager {
  constructor() {
    this.listeners = new WeakMap();
  }

  addEventListener(element, event, handler) {
    if (!this.listeners.has(element)) {
      this.listeners.set(element, new Map());
    }

    const elementListeners = this.listeners.get(element);
    if (!elementListeners.has(event)) {
      elementListeners.set(event, new Set());
    }

    elementListeners.get(event).add(handler);
    element.addEventListener(event, handler);
  }

  removeEventListener(element, event, handler) {
    if (!this.listeners.has(element)) return;

    const elementListeners = this.listeners.get(element);
    if (!elementListeners.has(event)) return;

    elementListeners.get(event).delete(handler);
    element.removeEventListener(event, handler);
  }
}

WeakSet

WeakSet is similar to Set but with the same limitations as WeakMap:

  • Values must be objects
  • Values are weakly held
  • Not iterable
  • No size property

WeakSet Usage

// Creating WeakSet
const ws = new WeakSet();

const obj1 = { a: 1 };
const obj2 = { b: 2 };

ws.add(obj1);
ws.add(obj2);
ws.add(obj1); // No effect (already exists)

console.log(ws.has(obj1)); // true
ws.delete(obj1);
console.log(ws.has(obj1)); // false

// Practical example: Tracking objects
class CircularReferenceDetector {
  constructor() {
    this.seen = new WeakSet();
  }

  check(obj) {
    if (typeof obj !== 'object' || obj === null) {
      return false;
    }

    if (this.seen.has(obj)) {
      return true; // Circular reference detected
    }

    this.seen.add(obj);

    for (const key in obj) {
      if (this.check(obj[key])) {
        return true;
      }
    }

    return false;
  }
}

// Marking objects as processed
const processed = new WeakSet();

function processOnce(obj, processFn) {
  if (processed.has(obj)) {
    console.log('Already processed');
    return;
  }

  processed.add(obj);
  processFn(obj);
}

Performance Comparisons

// Map vs Object performance
const iterations = 1000000;

// Object operations
console.time('Object creation');
const obj = {};
for (let i = 0; i < iterations; i++) {
  obj[`key${i}`] = i;
}
console.timeEnd('Object creation');

// Map operations
console.time('Map creation');
const map = new Map();
for (let i = 0; i < iterations; i++) {
  map.set(`key${i}`, i);
}
console.timeEnd('Map creation');

// Lookup performance
const lookupKey = `key${iterations / 2}`;

console.time('Object lookup');
for (let i = 0; i < 1000; i++) {
  const value = obj[lookupKey];
}
console.timeEnd('Object lookup');

console.time('Map lookup');
for (let i = 0; i < 1000; i++) {
  const value = map.get(lookupKey);
}
console.timeEnd('Map lookup');

// Delete performance
console.time('Object delete');
delete obj[lookupKey];
console.timeEnd('Object delete');

console.time('Map delete');
map.delete(lookupKey);
console.timeEnd('Map delete');

When to Use Each

Use Map when:

  • Keys are unknown until runtime
  • Keys are not strings or symbols
  • You need to frequently add and remove key-value pairs
  • You need to iterate in insertion order
  • You need the size property

Use Object when:

  • You have a fixed schema
  • You need JSON serialization
  • You're working with records/dictionaries
  • You need prototype chain features

Use Set when:

  • You need to store unique values
  • You need to check if a value exists quickly
  • You need set operations (union, intersection)
  • Order of elements matters

Use Array when:

  • You need indexed access
  • Order is important and you need to reorder
  • You need array methods (map, filter, reduce)
  • You have duplicate values

Use WeakMap/WeakSet when:

  • You need to attach data to objects without preventing garbage collection
  • You're implementing private properties
  • You need to track objects temporarily

Best Practices

  1. Choose the right data structure for your use case
  2. Use weak collections for metadata and private data
  3. Consider memory implications when storing large amounts of data
  4. Leverage built-in methods instead of reimplementing functionality
  5. Be aware of iteration order (insertion order for Map/Set)
  6. Use type checking when necessary (especially with Map keys)

Conclusion

Map and Set provide powerful alternatives to objects and arrays for specific use cases. Understanding when and how to use these modern collection types can lead to cleaner, more efficient code. WeakMap and WeakSet offer unique capabilities for managing object references without preventing garbage collection, making them invaluable for certain patterns like private properties and metadata management.