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!