JavaScript basicsMaps in plain wordsOne tool at a time
Map
Toolbox
A Map pairs each key with a value, like a phone book pairs names with numbers. This guide shows how to fill one, read it, loop through it, convert it and use it for counting and caching, with a small example for each that you can run.
Make and read
Create a Map, put values in with set, and read them back with get.
A Map is a list of key and value pairs, like a phone book: look up a name, get a number. It does the same job as a plain object, but it is built for it. Keys can be any type, it always knows its size, and it remembers the order you added things.
Picture it: one Map
prices"tea"20"coffee"45"juice"60prices.get("coffee")45Ask with a key, get its value.Makes an empty map, or one filled from a list of [key, value] pairs.
See the exampleAdds the pair, or replaces the value if the key is already there.
See the exampleReads the value stored under that key.
See the exampleChecks if the key is in the map.
See the exampleCounts the entries. It is a property, so no brackets.
See the exampleconst prices = new Map();
prices.set("tea", 20); // add a key and its value
prices.set("coffee", 45).set("juice", 60); // set gives the map back, so you can chain
console.log(prices.get("coffee")); // read a value
console.log(prices.get("soda")); // a missing key gives undefined
console.log(prices.has("tea")); // check a key exists
console.log(prices.size); // how many entries
console.log(prices);
Easy way to remember: set puts in, get takes out. Because set returns the map, you can chain several sets on one line.
$ node make.js 45 undefined true 3 Map(3) { 'tea' => 20, 'coffee' => 45, 'juice' => 60 }
// start with a list of [key, value] pairs
const roles = new Map([
["asha", "admin"],
["ravi", "editor"],
]);
console.log(roles.get("asha"), roles.size);
// keys can be any type, not just text
const labels = new Map([
[1, "number one"],
["1", "text one"],
[true, "a boolean"],
]);
console.log(labels.get(1), "|", labels.get("1"), "|", labels.get(true));
Why it matters: the number 1 and the text "1" are two different keys in a Map. In a plain object they would collide.
$ node start.js admin 2 number one | text one | a boolean
Map or object?
Know when a Map is the better choice than a plain object, and when it is not.
| Question | Plain object | Map |
|---|---|---|
| What can a key be? | text or symbol only | anything, even objects |
| Does it keep your order? | mostly, but numbers jump first | always, in the order added |
| How do I count it? | Object.keys(obj).length | map.size |
| Can I loop it directly? | no, use Object.entries | yes, with for...of |
| Hidden keys to trip on? | yes, like toString | none |
| Works with JSON? | Yes | No convert first |
| Best for | fixed shapes like a user record | lookups that grow and shrink |
// an object turns every key into text
const obj = {};
obj[1] = "number";
obj["1"] = "text"; // overwrites, because 1 became "1"
console.log(Object.keys(obj), obj[1]);
// a map keeps 1 and "1" apart
const map = new Map();
map.set(1, "number").set("1", "text");
console.log(map.size, map.get(1), map.get("1"));
// objects as keys: the object version breaks, the map works
const user = { id: 7 };
const seen = {};
seen[user] = "visited";
console.log(Object.keys(seen)); // the key became "[object Object]"
const visits = new Map([[user, "visited"]]);
console.log(visits.get(user));
Rule of thumb: use an object when you know the keys ahead of time. Use a Map when keys come from data, change often, or are not text.
$ node versus.js [ '1' ] text 2 number text [ '[object Object]' ] visited
Change and remove
Update a value, remove one entry, or empty the whole map.
Picture it: delete
cart"pen"5"book"1cart.delete("book")cart"pen"5The pair is gone and size drops by one.Setting a key that exists replaces its value. Size stays the same.
See the exampleRemoves that key and its value.
See the exampleRemoves every entry at once.
See the exampleconst cart = new Map([["pen", 2], ["book", 1]]);
cart.set("pen", 5); // same key again: the value is replaced
console.log(cart.get("pen"), cart.size);
console.log(cart.delete("book")); // true: it was there and is now gone
console.log(cart.delete("bag")); // false: nothing to remove
console.log(cart);
cart.clear(); // empty the whole map
console.log(cart.size);
Easy way to remember: delete tells you whether it found something, so you can use its answer in an if.
$ node change.js 5 2 true false Map(1) { 'pen' => 5 } 0
Loop through
Walk through every pair in the order you added them, or pull out just the keys or values.
Picture it: spread a Map
scores"asha"91"ravi"78[...scores]["asha", 91]["ravi", 78]A Map spreads into [key, value] pairs.Loops over every pair and unpacks each into two names.
See the exampleLists the keys. Spread it into an array with [...map.keys()].
See the exampleLists the values.
See the exampleLists the pairs. This is what for...of uses by default.
See the exampleRuns your helper on every pair. Note the value comes first.
See the exampleconst scores = new Map([["asha", 91], ["ravi", 78], ["meera", 85]]);
// for...of gives [key, value] pairs, in the order you added them
for (const [name, score] of scores) {
console.log(`${name}: ${score}`);
}
console.log([...scores.keys()]); // just the keys
console.log([...scores.values()]); // just the values
// forEach hands you the value first, then the key
scores.forEach((score, name) => {
if (score > 80) console.log(name, "passed with", score);
});
Watch for this: keys, values and entries give you an iterator, not an array. Wrap them in [...] when you want array tools like map or filter.
$ node loop.js asha: 91 ravi: 78 meera: 85 [ 'asha', 'ravi', 'meera' ] [ 91, 78, 85 ] asha passed with 91 meera passed with 85
Convert and save
Move data between Maps, objects, arrays and JSON without losing anything.
| From | To | Use |
|---|---|---|
| Object | Map | new Map(Object.entries(obj)) |
| Map | Object | Object.fromEntries(map) |
| Map | Array of pairs | [...map] |
| Map | JSON text | JSON.stringify([...map]) |
| JSON text | Map | new Map(JSON.parse(text)) |
const settings = { theme: "dark", size: 16 };
const asMap = new Map(Object.entries(settings)); // object to map
console.log(asMap.get("theme"));
const backToObject = Object.fromEntries(asMap); // map to object
console.log(backToObject);
console.log([...asMap]); // map to array of pairs
// JSON does not understand maps
console.log(JSON.stringify(asMap)); // {} : the data is lost
console.log(JSON.stringify([...asMap])); // save it as pairs instead
const restored = new Map(JSON.parse('[["theme","dark"],["size",16]]'));
console.log(restored.get("size"));
Why it matters: JSON.stringify(map) quietly gives {}. Always turn the Map into pairs before you save or send it.
$ node convert.js dark { theme: 'dark', size: 16 } [ [ 'theme', 'dark' ], [ 'size', 16 ] ] {} [["theme","dark"],["size",16]] 16
Count and group
Count how often things appear, and sort a list into named buckets.
Picture it: counting
thecatthedogthecounts.set(w, (counts.get(w) ?? 0) + 1)counts"the"3"cat"1"dog"1New words start at 0, then go up by one.Reads the current count, or 0 if the key is new.
See the examplePuts each item into a bucket named by what your helper returns.
See the exampleconst words = "the cat saw the dog and the cat ran".split(" ");
const counts = new Map();
for (const word of words) {
counts.set(word, (counts.get(word) ?? 0) + 1); // start at 0 if new
}
console.log(counts.get("the"), counts.get("cat"), counts.get("bird"));
// sort by count, biggest first, and keep the top three
const top = [...counts].sort((a, b) => b[1] - a[1]).slice(0, 3);
console.log(top);
The pattern: read the old value with a fallback, add to it, and set it back. It works for totals, counts and running sums.
$ node count.js 3 2 undefined [ [ 'the', 3 ], [ 'cat', 2 ], [ 'saw', 1 ] ]
const orders = [
{ id: 1, status: "paid" },
{ id: 2, status: "pending" },
{ id: 3, status: "paid" },
];
// Map.groupBy puts each item in a bucket named by what your helper returns
const byStatus = Map.groupBy(orders, (o) => o.status);
console.log([...byStatus.keys()]);
console.log(byStatus.get("paid").map((o) => o.id));
Needs: Node 21 or newer, or any current browser. Unlike Object.groupBy, the bucket names can be any type.
$ node group.js [ 'paid', 'pending' ] [ 1, 3 ]
WeakMap
Attach extra data to objects without keeping those objects alive forever.
A normal Map holds on to its keys, so an object used as a key never gets cleaned up while it sits in the map. A WeakMap holds its keys loosely. Once nothing else in your program uses that object, the entry can disappear by itself.
| Can you | Map | WeakMap |
|---|---|---|
| Use text or numbers as keys | Yes | No |
| Use objects as keys | Yes | Yes |
| Check size or loop over it | Yes | No |
| Let unused keys be cleaned up | No | Yes |
// a WeakMap only takes objects as keys, and forgets them
// when nothing else in your program is using that object
const lastSeen = new WeakMap();
let session = { user: "asha" };
lastSeen.set(session, "10:42");
console.log(lastSeen.get(session), lastSeen.has(session));
try {
lastSeen.set("asha", "10:42"); // text is not allowed as a key
} catch (err) {
console.log(err.constructor.name + ":", err.message);
}
session = null; // the entry can now be cleaned up by the garbage collector
console.log(typeof lastSeen.size, typeof lastSeen.keys); // no size, no looping
When to reach for it: caching results per object, or storing private details about DOM elements or request objects that you do not own.
$ node weak.js 10:42 true TypeError: Invalid value used as weak map key undefined undefined
All together
Build a small cache that keeps only the most recently used items, using the order a Map remembers.
A cache stores answers so you do not have to fetch them twice. It cannot grow forever, so when it is full it throws away the item nobody has asked for in the longest time. A Map makes this easy, because its first key is always the oldest one.
// a small "least recently used" cache that keeps only the newest items
class LruCache {
constructor(limit) {
this.limit = limit;
this.items = new Map(); // a map remembers insertion order
}
get(key) {
if (!this.items.has(key)) return undefined;
const value = this.items.get(key);
this.items.delete(key); // move it to the end: most recent
this.items.set(key, value);
return value;
}
set(key, value) {
this.items.delete(key);
this.items.set(key, value);
if (this.items.size > this.limit) {
const oldest = this.items.keys().next().value; // the first key is the oldest
this.items.delete(oldest);
}
}
}
const cache = new LruCache(2);
cache.set("user:1", "Asha");
cache.set("user:2", "Ravi");
cache.get("user:1"); // user:1 is now the most recent
cache.set("user:3", "Meera"); // full, so the oldest (user:2) goes
console.log([...cache.items.keys()]);
console.log(cache.get("user:2"), cache.get("user:3"));
What each part uses: has and get from module 01, delete and set from 03 to move a key to the end, size to know when it is full, and keys from 04 to find the oldest.
$ node cache.js [ 'user:1', 'user:3' ] undefined Meera
# save any example as a file and run it with Node 22 $ node cache.js # or paste it into your browser console with F12
Which one do I need?
Start from what you want to do, then read across to the tool and what it gives back.
| I want to | Use | You get |
|---|---|---|
| Make a map | new Map() | an empty map |
| Start with data | new Map([[k, v], ...]) | a filled map |
| Add or update | map.set(k, v) | the same map |
| Read a value | map.get(k) | the value or undefined |
| Check a key | map.has(k) | true or false |
| Count entries | map.size | a number |
| Remove one | map.delete(k) | true or false |
| Empty it | map.clear() | an empty map |
| Loop over it | for (const [k, v] of map) | every pair, in order |
| Get an array of keys | [...map.keys()] | an array |
| Turn it into an object | Object.fromEntries(map) | a plain object |
| Save it as JSON | JSON.stringify([...map]) | JSON text |
| Group a list | Map.groupBy(list, fn) | a map of arrays |
| Tag objects without leaks | new WeakMap() | a weak map |