Warming up the neural circuits...
By the end of this chapter you will:
Map instead of a plain object {} — and why it's faster for some tasksSetWeakMap to attach data to elements without causing memory leaksFor years, developers used Plain Objects {} for everything — storing user data, caching responses, tracking IDs. But objects have real limitations:
.size property — you have to do Object.keys(obj).lengthObject.prototype's methods, which pollutes your iterationModern collections (Map and Set) were introduced in ES6 to solve these problems. They are purpose-built, high-performance data structures that behave predictably.
1. Key-Value Pair — A relationship where a unique key maps to a value. In a Map, unlike a plain object, the key can be absolutely anything — a string, a number, another object, even a function.
2. Uniqueness — In a Set, every value must be unique. If you try to add a duplicate, it's silently ignored. This makes Set the fastest way to deduplicate data.
3. SameValueZero Equality — How Maps and Sets compare values. It's like === but with one fix: NaN === NaN is false with ===, but a Set correctly treats two NaN values as the same.
4. Weak Reference — A "fragile" link to an object. If the object has no other references in your app, the garbage collector can delete it — even if it's still a key in a WeakMap. This prevents memory leaks.
5. Insertion Order — Unlike plain objects (which sort integer-like keys first), Maps and Sets always iterate in the order you added items.
Vue 3 uses Map internally to track reactive dependencies. Because Maps can use objects as keys, Vue can map a specific piece of reactive data to the specific side-effects that depend on it — without adding messy string IDs anywhere.
new Map() — Creating a Map// Empty map
const registry = new Map();
// Initialize with key-value pairs
| Feature | Plain Object {} | Map |
|---|---|---|
| Key Types | String or Symbol only | Any type |
| Insertion Order | Complex (integers sort first) | Strict insertion order |
| Size | Object.keys(o).length | .size property |
| Frequent add/remove | Slower | Faster |
| Directly iterable | No (need Object.entries) | Yes |
| JSON support | ✅ Native | Needs conversion |
| Mistake | What goes wrong | The fix |
|---|---|---|
map.get({ id: 1 }) returns undefined | Different object literal = different reference | Store the reference in a variable and reuse it |
JSON.stringify(myMap) returns {} | JSON doesn't support Maps natively | Convert first: JSON.stringify(Object.fromEntries(myMap)) |
| Using primitives as WeakMap keys | Throws TypeError: Invalid value | Only objects can be WeakMap keys |
forEach on Set receives (val, val) | Set's forEach passes (value, value, set) for Array compatibility | Ignore the second argument or use for...of |
Converting Set to array before every |
[1, 2, 2, 3, 1, 4, 3, 5][1, 2, 3, 4, 5]["apple", "banana", "apple", "cherry", "banana", "apple"]Map { "apple" => 3, "banana" => 2, "cherry" => 1 }setA = new Set([1,2,3,4]) and setB = new Set([3,4,5,6])Set { 3, 4 }true if two strings are anagrams (same characters, different order). Use a Map to count character frequencies."listen" and "silent"trueMap. A user can make at most maxCalls requests per windowMs milliseconds. Return true if allowed, false if blocked.rateLimiter("user:1", 3, 60000) — at most 3 calls per minutetrue for first 3 calls, false after that within the windowWhat key types can Map use that plain objects cannot?
.set(key, value) — Add or update an entry. Returns the Map itself, so you can chain.
const cache = new Map();
cache.set("user:1", { id: 1, name: "Alice" });
cache.set("user:2", { id: 2, name: "Bob" });
// Chaining
cache
.set("user:3", { id: 3, name: "Carol" })
.set("user:4", { id: 4, name: "Dave" });| Returns | Use when |
|---|---|
| The Map itself | Adding or updating a key-value pair |
.get(key) — Retrieve a value by its key. Returns undefined if the key doesn't exist.
cache.get("user:1"); // { id: 1, name: "Alice" }
cache.get("user:99"); // undefined (no crash).has(key) — Check if a key exists. O(1) — instant lookup.
cache.has("user:1"); // true
cache.has("user:99"); // false.delete(key) — Remove an entry. Returns true if it was there, false if not.
cache.delete("user:2"); // true
cache.delete("user:99"); // false.size — Number of entries (not a method — a property).
cache.size; // 3.clear() — Remove all entries.
cache.clear();
cache.size; // 0const dom = new Map();
const button = document.querySelector("#submit");
const input = document.querySelector("#email");
dom.set(button, { clicks: 0 });
dom.set(input, { lastValue: "" });
// Later — update click count
const data = dom.get(button);
data.clicks += 1;
dom.set(button, data);Without Map, you'd have to add an id to every and use a plain object with string keys. With Map, the element reference is the key.
const users = new Map([
["alice", { id: 1, role: "admin" }],
["bob", { id: 2, role: "user" }]
]);
// Keys only
for (const key of users.keys()) {
console.log(key); // "alice", "bob"
}
// Values only
for (const val of users.values()) {
console.log(val.role); // "admin", "user"
}
// Both (most common)
for (const [key, val] of users.entries()) {
console.log(`${key} is ${val.role}`);
}
// Spread to array
const allKeys = [...users.keys()];| Feature | Plain Object {} | Map |
|---|---|---|
| Key types | String / Symbol only | Any type (Object, Function, NaN...) |
| Key order | Complex (integers sort first) | Strict insertion order |
| Size | Object.keys(o).length | .size property |
| Frequent add/remove | Slower | Faster |
| JSON serialize | JSON.stringify works | Needs Object.fromEntries first |
| Prototype pollution | Risk with toString, hasOwn... | No inherited keys |
Decision tree:
I need a key-value store...
├── Keys are always simple strings → Plain object {} (simpler syntax)
├── Keys might be objects/functions → Map
├── Frequently adding/removing keys → Map
├── Need .size instantly → Map
└── Need JSON.stringify support → Plain object {}new Set() — Creating a Set// Empty set
const visited = new Set();
// Initialize from an array (duplicates removed instantly)
const tags = new Set(["js", "react", "js", "css", "react"]);
// Set { "js", "react", "css" } — only 3 unique items.add(value) — Add a value. If it already exists, nothing happens. Returns the Set (chainable).
const permissions = new Set();
permissions.add("read").add("write").add("read"); // "read" added twice
permissions.size; // 2 — only stored once.has(value) — O(1) existence check. Far faster than arr.includes() for large datasets.
permissions.has("read"); // true
permissions.has("delete"); // false.delete(value) — Remove a value. Returns true if it was there.
permissions.delete("write"); // true
permissions.delete("admin"); // false.size — Number of unique values.
// Remove duplicates from an array in ONE line
const withDuplicates = [1, 2, 2, 3, 1, 4, 3];
const unique = [...new Set(withDuplicates)];
// [1, 2, 3, 4]
//
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// Union: all values from both
const union = new Set
Real-world example — find users who haven't onboarded:
const allUserIds = new Set(await db.getAllUserIds());
const onboardedIds = new Set(await db.getOnboardedUserIds());
const notOnboarded = new Set(
[...allUserIds].filter
| Task | Array | Set |
|---|---|---|
Existence check (includes) | O(n) — scans every item | O(1) — instant |
| Deduplication | Needs filter + indexOf | One-liner: new Set(arr) |
| Preserves duplicates | ✅ Yes | ❌ No |
access (arr[0]) | ✅ Yes | ❌ No |
| Order | By index | By insertion |
// ❌ Memory leak example
const metaMap = new Map();
function processElement(el) {
metaMap.set(el, { processedAt: Date.now() });
// Do work...
}
// Later, the element is removed from the DOM
document.querySelector("#old-widget").remove();
// BUT — metaMap still holds a reference to that element!
// The garbage collector cannot free the element's memory.
// Over time, this bloats memory.WeakMap — The fixA WeakMap holds its keys weakly. If the key object has no other references in your program, the garbage collector can reclaim its memory — and the WeakMap entry disappears automatically.
const metaMap = new WeakMap(); // ✅ Use WeakMap instead
function processElement(el) {
metaMap.set(el, { processedAt: Date.now() });
}
// When the element is removed from DOM and has no other refs...
Key restriction: Keys must be objects (or non-registered Symbols in ES2023+). You cannot use strings, numbers, or other primitives as WeakMap keys.
const wm = new WeakMap();
wm.set("string-key", "value"); // ❌ TypeError!
wm.set({}, "value"); // ✅ Object key — worksNot iterable: You cannot loop over a WeakMap or get its size. This is by design — if you could list all entries, the engine couldn't safely garbage collect them.
wm.size; // undefined
[...wm]; // ❌ TypeError: wm is not iterableBefore ES2022 private fields (#), WeakMap was the standard way to store truly private data:
const _balance = new WeakMap();
const _txHistory = new WeakMap();
class BankAccount {
constructor(initialBalance) {
_balance.set(this, initialBalance);
_txHistory.set(
WeakSet — Set of weakly-held objectsLike WeakMap but only stores values (no key-value pairs). Useful for tracking which objects have been "seen" or "processed" without preventing garbage collection.
const processed = new WeakSet();
function processOnce(task) {
if (processed.has(task)) return; // Skip if already processed
// Do work...
processed.add(task
I need to store data associated with an object...
├── I need to iterate over all entries → Map
├── I need to count entries (.size) → Map
├── Keys are primitives (strings, numbers) → Map
└── Keys are objects, and I worry about
memory leaks when objects are removed → WeakMapUsing a Set for selected IDs is more efficient and readable than searching through an array.
function SelectionList({ items }) {
const [selectedIds, setSelectedIds] = useState(new Set());
const toggleSelection = (id) => {
setSelectedIds(prev => {
const next = new Set(prev); // 1. Immutable copy
if (next.has(id)) next.delete(id); // 2. O(1) check & remove
else next.add(id); // 3. Add
return next; // 4. New reference → React re-renders
});
};
return (
<ul>
{items.map(item => (
<li
key={item.id}
className={selectedIds.has(item.id) ? "selected" : ""}
onClick={() => toggleSelection(item.id)}
>
{item.name}
</li>
))}
</ul>
);
}| Converts O(1) to O(n) |
| Keep it as a Set — only spread when you need array methods |
| Map of same key string looks wrong | Map uses SameValueZero — "1" !== 1 | Check key types: map.set(1, ...) and map.get("1") are different! |