The JavaScript Map Data Structure
By Flavio Copes
A complete guide to JavaScript Map: choosing it over Object, using any value as a key, iterating in order, converting data, and using WeakMap.
What is a Map
A Map data structure allows to associate data to a key.
Each key appears once and points to one value. Unlike a plain object, a Map accepts keys of any JavaScript type and gives us a focused API for adding, reading, deleting, and iterating entries.
Before ES6
ECMAScript 6 (also called ES2015) introduced the Map data structure to the JavaScript world, along with Set
Before its introduction, people generally used objects as maps, by associating some object or value to a specific key value:
const car = {}
car['color'] = 'red'
car.owner = 'Flavio'
console.log(car['color']) //red
console.log(car.color) //red
console.log(car.owner) //Flavio
console.log(car['owner']) //Flavio
Enter Map
ES6 introduced the Map data structure, providing us a proper tool to handle this kind of data organization.
A Map is initialized by calling:
const m = new Map()
Add items to a Map
You can add items to the map by using the set method:
m.set('color', 'red')
m.set('age', 2)
Get an item from a map by key
And you can get items out of a map by using get:
const color = m.get('color')
const age = m.get('age')
Delete an item from a map by key
Use the delete() method:
m.delete('color')
Delete all items from a map
Use the clear() method:
m.clear()
Check if a map contains an item by key
Use the has() method:
const hasColor = m.has('color')
Find the number of items in a map
Use the size property:
const size = m.size
Initialize a map with values
You can initialize a map with a set of values:
const m = new Map([['color', 'red'], ['owner', 'Flavio'], ['age', 2]])
Map keys
Just like any value (object, array, string, number) can be used as the value of the key-value entry of a map item, any value can be used as the key, even objects.
If you try to get a non-existing key using get() out of a map, it will return undefined.
How Map compares keys
const m = new Map()
m.set(NaN, 'test')
m.get(NaN) //test
const m = new Map()
m.set(+0, 'test')
m.get(-0) //test
Iterating over a map
Iterate over map keys
Map offers the keys() method we can use to iterate on all the keys:
for (const k of m.keys()) {
console.log(k)
}
Iterate over map values
The Map object offers the values() method we can use to iterate on all the values:
for (const v of m.values()) {
console.log(v)
}
Iterate over map key, value pairs
The Map object offers the entries() method we can use to iterate on all the values:
for (const [k, v] of m.entries()) {
console.log(k, v)
}
which can be simplified to
for (const [k, v] of m) {
console.log(k, v)
}
Convert to array
Convert the map keys into an array
const a = [...m.keys()]
Convert the map values into an array
const a = [...m.values()]
Map or Object?
Both can associate keys with values, but they are not interchangeable.
Use a Map when:
- keys are not limited to strings and symbols
- entries are added and removed frequently
- you need the number of entries
- insertion order matters during iteration
- the data is a collection rather than a record with named fields
Use an object when the keys describe a fixed shape:
const user = {
name: 'Flavio',
role: 'author',
}
This object represents one user. A map is a better fit for a changing collection of users:
const users = new Map()
users.set(42, { name: 'Flavio' })
users.set(87, { name: 'Ada' })
Objects also inherit from a prototype unless you deliberately create one without it. That means names such as toString already have meaning. Map does not have that ambiguity because entries live separately from its methods.
Keys use identity
Primitive keys match by value:
const settings = new Map()
settings.set('theme', 'dark')
settings.get('theme') //'dark'
Object keys match by identity. Two objects with the same properties are still different keys:
const visits = new Map()
const page = { slug: 'javascript' }
visits.set(page, 3)
visits.get(page) //3
visits.get({ slug: 'javascript' }) //undefined
Keep the original key reference when you plan to retrieve the value later. If your data already has a stable string or numeric identifier, that identifier is often a simpler key.
Map key equality follows the SameValueZero algorithm. That is why NaN can retrieve a NaN key and why positive and negative zero refer to the same entry.
Setting a key twice replaces its value
Calling set() with an existing key updates the value. It does not add a duplicate:
const stock = new Map()
stock.set('notebook', 3)
stock.set('notebook', 5)
stock.size //1
stock.get('notebook') //5
set() returns the map, so calls can be chained:
const colors = new Map()
.set('error', 'red')
.set('success', 'green')
I usually prefer the array constructor when all initial entries are known. It is easier to scan and creates smaller diffs.
Iteration keeps insertion order
A map iterates in the order keys were first inserted. Updating an existing value does not move its key. Deleting a key and adding it again places it at the end.
The default iterator returns [key, value] pairs, which is why destructuring works:
for (const [name, quantity] of stock) {
console.log(name, quantity)
}
forEach() is also available, but notice its argument order: value first, key second.
stock.forEach((quantity, name) => {
console.log(name, quantity)
})
I prefer for...of when the loop needs break, continue, or await. forEach() cannot provide those control-flow behaviors.
Convert between maps, arrays, and objects
Spread a map to get an array of entries:
const entries = [...stock]
//[['notebook', 5]]
Build a map from transformed entries:
const prices = new Map([
['notebook', 8],
['pen', 2],
])
const pricesWithTax = new Map(
[...prices].map(([name, price]) => [name, price * 1.2])
)
When every key is a string or symbol, Object.fromEntries() converts a map to an object:
Object.fromEntries(prices)
//{ notebook: 8, pen: 2 }
Be careful with non-string keys. Object property keys are strings or symbols, so converting a map can change or collapse keys.
Map and JSON
JSON.stringify() does not serialize map entries:
JSON.stringify(new Map([['color', 'orange']])) //'{}'
Convert it explicitly based on the shape you want:
JSON.stringify([...prices])
//'[["notebook",8],["pen",2]]'
The entry-array form preserves key types that JSON itself supports. The object form is friendlier to many APIs but only makes sense for string keys.
A practical counting pattern
A map is perfect for a frequency table:
const words = ['map', 'set', 'map', 'array', 'map']
const counts = new Map()
for (const word of words) {
counts.set(word, (counts.get(word) ?? 0) + 1)
}
counts.get('map') //3
The nullish coalescing operator matters here. A missing key returns undefined, while a stored value could legitimately be 0, false, or an empty string.
WeakMap is for metadata tied to object lifetime
A WeakMap does not keep its object keys alive. This makes it useful for attaching metadata to objects without controlling how long those objects remain in memory:
const metadata = new WeakMap()
function track(element) {
metadata.set(element, { clicks: 0 })
}
When nothing else references an element, the engine may collect it together with its metadata. We cannot observe when that happens, which is exactly why a WeakMap has no iterator, size, or clear().
Do not use WeakMap as a general-purpose cache with string keys. Use it when the key is an object and the value should live no longer than that object.
The exact behavior of Map and WeakMap is defined in the ECMAScript keyed collections specification. For a collection of unique values rather than key-value pairs, read my JavaScript Set guide.
Merge maps
Because a map constructor accepts entries, spreading provides a concise merge:
const defaults = new Map([
['theme', 'light'],
['fontSize', 16],
])
const saved = new Map([
['theme', 'dark'],
])
const settings = new Map([...defaults, ...saved])
Later entries replace earlier entries with the same key, so the saved theme wins. This is a shallow merge. If both maps store nested objects, those objects are still shared references.
Sort a map by sorting its entries
A map preserves insertion order but does not sort itself. Convert it to entries, sort them, and create a new map:
const scores = new Map([
['Ada', 92],
['Linus', 85],
['Grace', 98],
])
const ranked = new Map(
[...scores].sort((a, b) => b[1] - a[1])
)
The original map remains unchanged. Use a locale-aware collator when sorting human names rather than comparing strings with < and >.
Use nested maps for two-dimensional keys
Concatenating two identifiers into one string can create collisions and escaping problems. A nested map keeps each key separate:
const permissions = new Map()
function setPermission(userId, projectId, role) {
if (!permissions.has(userId)) {
permissions.set(userId, new Map())
}
permissions.get(userId).set(projectId, role)
}
setPermission(42, 7, 'editor')
permissions.get(42).get(7) //'editor'
If the combination has a natural object identity, an object key can work too. The important part is choosing a key you can retrieve later.
Common Map mistakes
Do not use bracket notation to add entries:
const map = new Map()
map['color'] = 'orange'
map.has('color') //false
map.size //0
That code creates a normal property on the Map object. It does not create a map entry. Always use set() and get().
Do not test get() alone when undefined is a valid stored value:
const map = new Map([['result', undefined]])
map.get('result') //undefined
map.has('result') //true
Use has() to distinguish a missing key from a key whose value is undefined.
Finally, remember that copying a map is shallow:
const original = new Map([
['user', { name: 'Flavio' }],
])
const copy = new Map(original)
copy.get('user').name = 'Ada'
original.get('user').name //'Ada'
The map structure is new, but its object keys and values are the same references. If you need an independent data graph, decide how each value should be cloned rather than assuming the map constructor does it.
Related posts about js: