JavaScript Maps
Maps in JavaScript are a mutable collection of key-value pairs, where keys can be of any type. Maps are particularly useful for managing collections of items with unique keys.
Map vs Object: Which Should You Use?
Short answer: use a Map for a dynamic key-value collection, especially when keys are objects or their original types matter. Use a plain object for a record with named fields, such as a user profile or JSON payload.
| Question | Map | Plain object |
|---|---|---|
| Which keys can I use? | Any JavaScript value | Strings and symbols; other keys are converted |
| How do I iterate? | Directly with for...of | Use an appropriate API such as Object.entries() |
| How do I count entries? | .size | For own enumerable string keys, Object.keys(obj).length |
| What is the order? | Key insertion order | Defined rules depend on key kind and enumeration API |
| How do I serialize to JSON? | Convert to a suitable representation first | JSON.stringify() supports ordinary records directly |
Do not choose Map simply because someone says objects are unordered or maps are always faster. Explain the data model and operations first.
const map = new Map()
map.set(1, 'number key')
map.set('1', 'string key')
console.log(map.size) // Output: 2
const record = {}
record[1] = 'number key'
record['1'] = 'string key'
console.log(Object.keys(record).length) // Output: 1
Interview check: why do these collections have different sizes? The object converts the numeric property key to a string; the Map retains two distinct keys. For object properties and prototypes, see Objects. This lesson owns Map operations and their trade-offs.
Concept and Use Cases
Definition:
A JavaScript Map is a collection of key-value pairs, where each key can be of any type (primitive or object). Maps maintain the order of insertion and provide a range of methods to manage and manipulate key-value pairs.
Common Use Cases:
- Storing and managing collections of items with unique keys.
- Implementing caches and lookup tables.
- Efficiently managing dynamic collections with non-string keys.
- Associating metadata with objects.
When to Use
- When you need to store key-value pairs and require keys that are not limited to strings.
- When you need to preserve the insertion order of entries.
- When performing frequent updates and lookups on a dynamic collection of data.
Time and Space Complexity
Time Complexity:
- Insertion: typically O(1) average in a hash-table implementation
- Deletion: typically O(1) average in a hash-table implementation
- Search: typically O(1) average in a hash-table implementation
- Iteration: O(n)
These are common implementation assumptions, not universal worst-case guarantees. ECMAScript requires average sublinear access, not a particular hash-table representation. See MDN's Map reference.
Space Complexity:
- O(n), where n is the number of key-value pairs in the map.
Map Operations and Methods
Creating Maps
Example:
let emptyMap = new Map();
let numberMap = new Map([[1, 'one'], [2, 'two'], [3, 'three']]);
let mixedMap = new Map([
['stringKey', 'value1'],
[42, 'value2'],
[{key: 'object'}, 'value3']
]);
Adding and Removing Entries
Example:
let map = new Map();
map.set('name', 'John');
map.set('age', 30);
map.set('age', 31); // Updates the value for 'age'
console.log(map); // Output: Map { 'name' => 'John', 'age' => 31 }
map.delete('name');
console.log(map); // Output: Map { 'age' => 31 }
Checking for Existence
Example:
let map = new Map([['name', 'John'], ['age', 30]]);
console.log(map.has('name')); // Output: true
console.log(map.has('address')); // Output: false
Getting the Size of a Map
Example:
let map = new Map([['name', 'John'], ['age', 30]]);
console.log(map.size); // Output: 2
Iterating Over Maps
Example:
let map = new Map([['name', 'John'], ['age', 30]]);
for (let [key, value] of map) {
console.log(`${key}: ${value}`);
}
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
Clearing a Map
Example:
let map = new Map([['name', 'John'], ['age', 30]]);
map.clear();
console.log(map.size); // Output: 0
Converting Maps to Arrays
Example:
let map = new Map([['name', 'John'], ['age', 30]]);
let keysArray = Array.from(map.keys());
let valuesArray = Array.from(map.values());
let entriesArray = Array.from(map.entries());
console.log(keysArray); // Output: ['name', 'age']
console.log(valuesArray); // Output: ['John', 30]
console.log(entriesArray); // Output: [['name', 'John'], ['age', 30]]
Practical Tips and Tricks
-
Using Maps for Object Metadata: Maps can be used to associate metadata with objects without modifying the objects.
Example:
let obj1 = {id: 1}; let obj2 = {id: 2}; let metadataMap = new Map(); metadataMap.set(obj1, {role: 'admin'}); metadataMap.set(obj2, {role: 'user'}); console.log(metadataMap.get(obj1)); // Output: { role: 'admin' } -
Using Complex Keys: Maps allow for keys that are not limited to strings, such as objects or functions.
Example:
let map = new Map(); let key1 = {}; let key2 = () => {}; map.set(key1, 'value1'); map.set(key2, 'value2'); console.log(map.get(key1)); // Output: 'value1' console.log(map.get(key2)); // Output: 'value2' -
Efficient Lookup Table: Maps provide efficient lookups under the implementation assumptions above, making them useful for lookup tables. Use
has(key)when you must distinguish a missing key from a storedundefinedvalue.Example:
let lookup = new Map([['a', 1], ['b', 2], ['c', 3]]); console.log(lookup.get('b')); // Output: 2
Common Gotchas
-
Using Objects as Keys: When using objects as keys, the same object reference must be used for lookups.
Example:
let map = new Map(); let obj = {id: 1}; map.set(obj, 'value'); console.log(map.get({id: 1})); // Output: undefined (different object reference) console.log(map.get(obj)); // Output: 'value' -
Order of Insertion: Maps maintain the order of insertion, which can affect iteration and operations.
Example:
let map = new Map([['a', 1], ['b', 2], ['c', 3]]); console.log(Array.from(map.keys())); // Output: ['a', 'b', 'c'] -
Not Using
clearCorrectly: Theclearmethod removes all entries from a map but doesn't return the map itself.Example:
let map = new Map([['a', 1], ['b', 2]]); map.clear(); console.log(map.size); // Output: 0
Advanced Topics
WeakMaps
WeakMaps support object and non-registered symbol keys without strongly retaining them. They are not enumerable. See Memory Management for reachability and collection caveats.
Example:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, 'value');
console.log(weakMap.get(obj)); // Output: 'value'
obj = null; // Removes this reference; collection timing is not guaranteed
Map Operations Using Utility Functions
Example:
function mergeMaps(map1, map2) {
return new Map([...map1, ...map2]);
}
function mapKeysToArray(map) {
return Array.from(map.keys());
}
function mapValuesToArray(map) {
return Array.from(map.values());
}
let map1 = new Map([['a', 1], ['b', 2]]);
let map2 = new Map([['c', 3], ['d', 4]]);
let mergedMap = mergeMaps(map1, map2);
console.log(mergedMap); // Output: Map { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 }
console.log(mapKeysToArray(mergedMap)); // Output: ['a', 'b', 'c', 'd']
console.log(mapValuesToArray(mergedMap)); // Output: [1, 2, 3, 4]
Map Algorithms
Below are some common map algorithms you should be familiar with:
Counting Frequencies
Counting frequencies involves determining how often each element appears in an array.
Example:
function countFrequencies(arr) {
let frequencyMap = new Map();
for (let item of arr) {
if (frequencyMap.has(item)) {
frequencyMap.set(item, frequencyMap.get(item) + 1);
} else {
frequencyMap.set(item, 1);
}
}
return frequencyMap;
}
let array = ['a', 'b', 'a', 'c', 'b', 'a'];
console.log(countFrequencies(array)); // Output: Map { 'a' => 3, 'b' => 2, 'c' => 1 }
Finding Unique Keys
Finding unique keys identifies elements that appear exactly once in an array.
Example:
function findUniqueKeys(arr) {
let map = new Map();
for (let item of arr) {
map.set(item, (map.get(item) || 0) + 1);
}
let uniqueKeys = [];
for (let [key, value] of map) {
if (value === 1) {
uniqueKeys.push(key);
}
}
return uniqueKeys;
}
let array = ['a', 'b', 'a', 'c', 'b', 'd'];
console.log(findUniqueKeys(array)); // Output: ['c', 'd']
Merging Multiple Maps
Merging multiple maps combines the key-value pairs of multiple maps into one.
Example:
function mergeMultipleMaps(...maps) {
return maps.reduce((merged, map) => {
for (let [key, value] of map) {
merged.set(key, value);
}
return merged;
}, new Map());
}
let map1 = new Map([['a', 1], ['b', 2]]);
let map2 = new Map([['c', 3], ['d', 4]]);
let map3 = new Map([['e', 5], ['f', 6]]);
let mergedMap = mergeMultipleMaps(map1, map2, map3);
console.log(mergedMap); // Output: Map { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6 }
Interview Tips and Tricks
-
Understand Map Methods: Be familiar with methods like
set,get,has,delete, andclear. -
Practice Map Use Cases: Work on problems involving counting frequencies, managing caches, and performing efficient lookups.
-
Handle Edge Cases: Consider scenarios with empty maps, maps with non-string keys, and maps with nested structures.
-
Use WeakMaps for Efficient Memory Management: Utilize WeakMaps when you need keys to be garbage collected.
Common Mistakes
-
Misusing Object Keys: Remember that objects used as keys must be the same reference for lookups to work.
-
Forgetting Order of Insertion: Maps maintain the order of insertion, which may affect iteration and operations.
-
Not Using
clearCorrectly: Theclearmethod removes all entries but does not return the map itself.
By mastering JavaScript maps and understanding their intricacies, you will be well-equipped to handle a variety of interview questions and real-world problems involving key-value pairs. Regular practice and a solid grasp of advanced topics will deepen your understanding and improve your problem-solving skills.
Practice Problems
Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.