You have been using plain objects as dictionaries — string keys, looked up by name. That works for simple cases, but objects were designed to model things with properties, not to be general-purpose lookup tables, and the cracks show the moment you push them. Their keys are silently coerced to strings, they have no built-in way to tell you how many entries they hold, and the order of their keys is a historical accident rather than a promise you can lean on.
JavaScript ships two collections built specifically for the jobs objects do badly. A Map is a key-value store where the key can be anything — a string, a number, even another object — and insertion order is guaranteed. A Set is a collection that holds each value exactly once. Reach for them when you are storing and looking things up, and reserve plain objects for modelling actual things.
flowchart TD M["Map keys"] --> K1["strings"] M --> K2["numbers"] M --> K3["objects"] M --> K4["even functions"] O["plain object keys"] --> S["all coerced to strings"] style M fill:#1e7f3d,color:#fff style O fill:#b91c1c,color:#fff
Map: a dictionary that plays fair
A Map is built around five methods you will use constantly. set(key, value) adds or updates an entry and returns the map, so calls chain. get(key) reads a value back (or undefined if absent), has(key) asks whether a key exists, delete(key) removes one entry, and size tells you how many entries there are — something a plain object makes you compute with Object.keys(obj).length.
Crucially, a Map never coerces its keys. Store a number key and it stays a number; store an object as a key and it is held by identity, so two different {} are two different keys. Insertion order is also preserved: when you loop over a Map, entries come out in the order you added them, every time.
const scores = new Map();
scores.set("Ada", 10);
scores.set("Grace", 7);
scores.set("Ada", 12); // updates the existing key
console.log(scores.get("Ada"));
console.log(scores.has("Grace"));
console.log(scores.size);
scores.delete("Grace");
console.log(scores.size);
Set: each value, once
A Set is the right tool whenever 'unique' is part of the requirement. Calling add(value) inserts a value, but only if it is not already there — a second add of the same value is a no-op, so the set never holds duplicates. You get the same has, delete, and size that Map offers, and like Map it preserves insertion order.
The most common Set move is de-duplicating an array in one breath: new Set(arr) builds a set from the array (collapsing repeats), and spreading it back out with [...set] gives you a fresh array of the unique values, in the order they first appeared. No loop, no temp object, no indexOf check — that single expression is why Sets earn their place.
flowchart LR I["in: 3,1,4,1,5,5"] --> S["Set"] S --> O["out: 3,1,4,5 (unique)"] style S fill:#e0900b,color:#fff style O fill:#1e7f3d,color:#fff
const seen = new Set(); [3, 1, 4, 1, 5, 9, 2, 6, 5].forEach((n) => seen.add(n)); console.log(seen.size); console.log(seen.has(9)); const unique = [...seen]; console.log(unique);
const objCache = {};
objCache[{}] = 100;
objCache[{}] = 200;
console.log(objCache[{}]); // 200 — both wrote the same string key
const map = new Map();
const a = {}, b = {};
map.set(a, 10).set(b, 20);
console.log(map.get(a));
console.log(map.get(b));
console.log(map.size);
Weak collections: keys that can vanish
There is a memory-minded cousin of each: WeakMap for key-value pairs and WeakSet for unique values. The 'weak' part means their keys (or values) must be objects, held so lightly that if nothing else in your program still references one, the garbage collector may reclaim it — and the entry quietly disappears from the collection on its own.
The price for that auto-cleanup is steep: a WeakMap has no size, no iteration, and no clearing, because its contents depend on garbage collection and so can never be enumerated. That makes WeakMap a specialist tool, usually for tucking private data against an object you do not own. For anything you need to count, look up by primitive, or loop over, a plain Map or Set remains the right choice — and the one you will reach for almost every day.
What does this print? Track what set and get do, and remember set returns the map.
const m = new Map();
m.set("a", 1).set("b", 2);
console.log(m.get("b"));
console.log(m.size);
set chains, so both keys land in the map.
get("b") returns 2, and size counts the entries.
Write charCount(str) that returns a Map mapping each character in str to how many times it appears. Use the character as the key; read its current count with get, defaulting to 0, then set the new count. So charCount("aba") returns a Map whose get('a') is 2 and get('b') is 1.
function charCount(str) {
// return a Map of character -> count
}
Start with
const counts = new Map();.For each character: counts.get(ch) || 0 gives the current count (0 if new).
counts.set(ch, that + 1) records the updated count.
function charCount(str) {
const counts = new Map();
for (const ch of str) {
counts.set(ch, (counts.get(ch) || 0) + 1);
}
return counts;
}
This memoizer caches a function's results by its argument, but it uses a plain object as the cache — and object keys are stringified, so two different object arguments both become "[object Object]" and collide, returning the wrong cached value. Replace the cache with a Map so object arguments keep their identity as keys (use has/get/set).
function memo(fn) {
const cache = {};
return function (arg) {
if (cache[arg]) return cache[arg];
const result = fn(arg);
cache[arg] = result;
return result;
};
}
A plain object turns object keys into the same string, so distinct args collide.
Use
new Map()and checkcache.has(arg)before computing.Store with
cache.set(arg, result); retrieve withcache.get(arg).
function memo(fn) {
const cache = new Map();
return function (arg) {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
Write unique(arr) that returns a new array of arr's values in first-occurrence order with duplicates removed. Do it with a Set: build a Set from arr (which collapses repeats) and spread it back into an array. So unique([1,2,2,3,1]) returns [1,2,3].
function unique(arr) {
// return a new array with duplicates removed, order preserved
}
new Set(arr)builds a set, dropping duplicates.Spread it back out:
[...new Set(arr)].Sets keep first-occurrence order, so the array order is preserved.
function unique(arr) {
return [...new Set(arr)];
}
You need to map each user object to its preferences. Why is a Map the right choice over a plain object?
Plain-object keys are coerced to strings, so two different user objects both become "[object Object]" and overwrite each other. A Map holds object keys by identity, so each user stays a distinct key.
Recap
- A
Mapis a key-value store whose keys can be any type, kept by identity, with a realsize, guaranteed insertion order, andset/get/has/delete. - A
Setholds each value once;[...new Set(arr)]de-duplicates an array in one expression. - A plain object stringifies its keys, so number
1and string"1"collide, and two object keys both become"[object Object]". That is the signal to switch toMap. WeakMapandWeakSethold object keys/values weakly so they can be garbage-collected, but they have nosizeand cannot be iterated — specialists for private data, not everyday lookups.
Next you'll learn destructuring, the concise syntax for pulling values out of exactly these objects and arrays.