Advanced JavaScript (ES6+)

Maps - Key-Value Pairs with Any Key Type

13 min Lesson 22 of 40

Maps - Key-Value Pairs with Any Key Type

ES6 introduced the Map object, a powerful key-value data structure that allows keys of any type. Unlike plain objects, Maps can use objects, functions, or any primitive as keys, and they maintain insertion order while providing better performance for frequent additions and deletions.

What is a Map?

A Map is a collection of key-value pairs where keys can be of any type. Key differences from objects:

  • Keys can be any type (objects, functions, primitives)
  • Maps maintain insertion order
  • Maps have a size property for quick length access
  • Maps are optimized for frequent additions and deletions
  • Maps are directly iterable
Key Fact: Unlike objects where keys are always strings or symbols, Map keys can be literally anything - even other objects or functions.

Creating Maps

You can create Maps in several ways:

// Empty Map const emptyMap = new Map(); // Map from array of [key, value] pairs const users = new Map([ [1, 'Alice'], [2, 'Bob'], [3, 'Charlie'] ]); // Map with different key types const mixedMap = new Map([ ['string', 'String key'], [42, 'Number key'], [true, 'Boolean key'], [{id: 1}, 'Object key'], [function() {}, 'Function key'] ]); // Convert object to Map const obj = {name: 'John', age: 30}; const mapFromObj = new Map(Object.entries(obj)); console.log(mapFromObj); // Map {'name' => 'John', 'age' => 30}

Map Methods

Maps provide intuitive methods for working with key-value pairs:

const map = new Map(); // set(key, value) - Add or update a key-value pair map.set('name', 'Alice'); map.set('age', 25); map.set('city', 'New York'); // Method chaining with set map.set('country', 'USA').set('email', 'alice@example.com'); // get(key) - Retrieve a value console.log(map.get('name')); // 'Alice' console.log(map.get('missing')); // undefined // has(key) - Check if key exists console.log(map.has('age')); // true console.log(map.has('phone')); // false // delete(key) - Remove a key-value pair map.delete('city'); console.log(map.has('city')); // false // size - Get number of entries console.log(map.size); // 4 // clear() - Remove all entries map.clear(); console.log(map.size); // 0
Tip: The set() method returns the Map itself, enabling method chaining: map.set('a', 1).set('b', 2).set('c', 3)

Using Objects as Keys

One of Map's most powerful features is the ability to use objects as keys:

// Using objects as keys const user1 = {id: 1, name: 'Alice'}; const user2 = {id: 2, name: 'Bob'}; const userRoles = new Map(); userRoles.set(user1, 'admin'); userRoles.set(user2, 'editor'); console.log(userRoles.get(user1)); // 'admin' console.log(userRoles.get(user2)); // 'editor' // DOM elements as keys const button1 = document.createElement('button'); const button2 = document.createElement('button'); const clickCounts = new Map(); clickCounts.set(button1, 0); clickCounts.set(button2, 0); button1.addEventListener('click', () => { clickCounts.set(button1, clickCounts.get(button1) + 1); }); // Functions as keys const fn1 = () => console.log('First'); const fn2 = () => console.log('Second'); const fnMetadata = new Map(); fnMetadata.set(fn1, {name: 'First Function', calls: 0}); fnMetadata.set(fn2, {name: 'Second Function', calls: 0});

Iterating Over Maps

Maps are iterable and maintain insertion order:

const fruits = new Map([ ['apple', 2], ['banana', 5], ['orange', 3] ]); // for...of with destructuring for (const [key, value] of fruits) { console.log(`${key}: ${value}`); } // forEach method fruits.forEach((value, key, map) => { console.log(`${key} => ${value}`); }); // Get all keys for (const key of fruits.keys()) { console.log(key); // apple, banana, orange } // Get all values for (const value of fruits.values()) { console.log(value); // 2, 5, 3 } // Get all entries for (const [key, value] of fruits.entries()) { console.log(`${key}: ${value}`); } // Convert to array const entriesArray = [...fruits]; // [['apple', 2], ['banana', 5], ...] const keysArray = [...fruits.keys()]; // ['apple', 'banana', 'orange'] const valuesArray = [...fruits.values()]; // [2, 5, 3]

Maps vs Objects

Understanding when to use Maps vs Objects is crucial:

Use Map when: - Keys are unknown until runtime - Keys are of different types (especially non-strings) - You need to frequently add/delete entries - You need to iterate in insertion order - You need the size of the collection Use Object when: - Keys are known and static - Keys are all strings or symbols - You need JSON serialization - You need to work with property access syntax (obj.prop) - You need to use object methods and prototypes
// Map advantages const map = new Map(); const objKey = {id: 1}; map.set(objKey, 'value'); // Object as key map.set(1, 'number'); // Number as key map.set('1', 'string'); // Different from number 1 console.log(map.size); // Built-in size // Object limitations const obj = {}; obj[objKey] = 'value'; // Converts to string '[object Object]' obj[1] = 'number'; // Converts to string '1' obj['1'] = 'string'; // Same as number 1 console.log(Object.keys(obj).length); // Manual size calculation
Important: Map keys are compared using SameValueZero algorithm. Two object keys are only equal if they reference the same object, not if they have the same properties.

WeakMap

WeakMap is a variant of Map with special memory management characteristics:

// WeakMap only accepts objects as keys const weakMap = new WeakMap(); let user = {name: 'Alice'}; weakMap.set(user, {loginCount: 5}); console.log(weakMap.has(user)); // true console.log(weakMap.get(user)); // {loginCount: 5} // When user is garbage collected, the WeakMap entry is automatically removed user = null; // After garbage collection, entry is gone WeakMap Characteristics: - Keys must be objects (not primitives) - Keys are held weakly (don't prevent garbage collection) - Not iterable (no keys(), values(), entries(), forEach()) - No size property - Perfect for private data and memory-sensitive caching

Practical Examples

// Example 1: Caching function results const cache = new Map(); function expensiveOperation(n) { if (cache.has(n)) { console.log('Cache hit!'); return cache.get(n); } console.log('Computing...'); const result = n * n; // Simulate expensive computation cache.set(n, result); return result; } console.log(expensiveOperation(5)); // Computing... 25 console.log(expensiveOperation(5)); // Cache hit! 25 // Example 2: Counting occurrences function countOccurrences(arr) { const counts = new Map(); for (const item of arr) { counts.set(item, (counts.get(item) || 0) + 1); } return counts; } const numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]; const counts = countOccurrences(numbers); console.log(counts); // Map {1 => 1, 2 => 2, 3 => 3, 4 => 4} // Example 3: Grouping by property function groupBy(arr, keyFn) { const groups = new Map(); for (const item of arr) { 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); // Map {25 => [{name: 'Alice', age: 25}, {name: 'Charlie', age: 25}], // 30 => [{name: 'Bob', age: 30}]}

Converting Between Maps and Objects

// 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); // Map to JSON const jsonStr = JSON.stringify([...map]); // JSON to Map const parsed = new Map(JSON.parse(jsonStr)); // Convert Map with object keys to array for JSON const complexMap = new Map([[{id: 1}, 'value']]); const serializable = [...complexMap].map(([key, value]) => ({ key: JSON.stringify(key), value: value }));

Practice Exercise:

Challenge: Create a function that returns the first non-repeating character in a string.

function firstNonRepeating(str) { const charCount = new Map(); // Count occurrences for (const char of str) { charCount.set(char, (charCount.get(char) || 0) + 1); } // Find first with count of 1 for (const char of str) { if (charCount.get(char) === 1) { return char; } } return null; } console.log(firstNonRepeating('aabbcdde')); // 'c' console.log(firstNonRepeating('aabbcc')); // null

Try it yourself: Modify this to return all non-repeating characters.

Summary

In this lesson, you learned:

  • Maps store key-value pairs with keys of any type
  • Map methods: set(), get(), has(), delete(), clear(), and size property
  • Maps maintain insertion order and are directly iterable
  • Objects can be used as Map keys, enabling powerful patterns
  • Maps vs Objects: Maps for dynamic keys, Objects for static properties
  • WeakMap for memory-efficient object-keyed storage
  • Practical uses: caching, counting, grouping, and metadata storage
Next Up: In the next lesson, we'll explore Symbols - unique and immutable identifiers!