JavaScript Cheatsheet
The array/object methods, the async patterns, and the scoping rules that actually cause bugs in production. For the full explanations, see the JavaScript guides.
The event loop, in one picture
The rule that explains almost every “why did this run in this order” question: after the call stack empties, JavaScript drains the entire microtask queue before touching a single macrotask. This is why Promise.resolve().then(...) always runs before a setTimeout(fn, 0), even though the timeout has “zero” delay — the microtask queue has priority, every time, no exceptions.
console.log("1");setTimeout(() => console.log("2"), 0);Promise.resolve().then(() => console.log("3"));console.log("4");// Output: 1, 4, 3, 2 — sync code first, then all microtasks, then macrotasksvar, let, and const
function example() { if (true) { var x = 1; // function-scoped — leaks out of the if-block let y = 2; // block-scoped — only exists inside { } const z = 3; // block-scoped, reassignment forbidden } console.log(x); // 1 — works, which surprises people console.log(y); // ReferenceError — y doesn't exist here}var being function-scoped instead of block-scoped is the source of the classic closure-in-a-loop bug:
// WRONG — logs 3, 3, 3 because var i is shared across all three closuresfor (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100);}
// RIGHT — let creates a fresh binding per iterationfor (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100);}const prevents reassignment of the variable binding, not mutation of what it points to — const arr = [1,2,3]; arr.push(4) is completely legal because arr still points at the same array; only arr = [5,6,7] would throw.
Destructuring and spread
const { name, age = 18, ...rest } = { name: "Alice", city: "NYC", country: "US" };// name = "Alice", age = 18 (default, not in object), rest = { city: "NYC", country: "US" }
const [first, second, ...others] = [1, 2, 3, 4, 5];// first = 1, second = 2, others = [3, 4, 5]
function greet({ name, greeting = "Hello" } = {}) { return `${greeting}, ${name}!`;}
const merged = { ...defaults, ...overrides }; // overrides wins on key collisionsconst combined = [...arr1, ...arr2]; // concatenate without .concat()The = {} default in the greet example matters: without it, calling greet() with no argument tries to destructure undefined and throws immediately, before the individual field defaults ever get a chance to apply.
Array methods — pick the right one
| Method | Returns | Use it for |
|---|---|---|
.map() | New array, same length | Transforming every element |
.filter() | New array, subset | Keeping elements that match a condition |
.reduce() | Any single value | Folding an array down to one thing (sum, object, another array) |
.forEach() | undefined | Side effects only — can’t break out of it, can’t return a value |
.find() | First matching element or undefined | Stop-on-first-match search |
.some() / .every() | Boolean | ”does at least one” / “do all” match |
.flatMap() | Flattened array | Map, then flatten one level — avoids a separate .flat() call |
const orders = [{ amount: 50 }, { amount: 120 }, { amount: 30 }];
const total = orders.reduce((sum, o) => sum + o.amount, 0); // 200const highValue = orders.filter(o => o.amount > 40).map(o => o.amount); // [50, 120]const hasLarge = orders.some(o => o.amount > 100); // true
// reduce building an object — the pattern that replaces a manual for-loop + objectconst byTier = orders.reduce((acc, o) => { const tier = o.amount > 100 ? "high" : "low"; (acc[tier] ??= []).push(o); return acc;}, {});.forEach() cannot be stopped early (no break equivalent) and its return value is always undefined — reaching for .find() or a plain for...of loop (which does support break) is the fix when you catch yourself trying to return out of a .forEach() callback to “stop” it.
Promises and async/await
function fetchUser(id) { return fetch(`/api/users/${id}`) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); });}
// async/await — same thing, reads top-to-bottom instead of chainedasync function fetchUser(id) { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json();}// Running requests in parallel — the mistake that halves nobody's load time until it's fixed// SLOW — each await blocks the next call from startingconst user = await fetchUser(1);const posts = await fetchPosts(1);
// FAST — both requests fire immediately, only the waiting happens togetherconst [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]);// Promise.allSettled — when one failure shouldn't cancel the restconst results = await Promise.allSettled([fetchUser(1), fetchUser(999)]);results.forEach(r => { if (r.status === "fulfilled") console.log(r.value); else console.error(r.reason);});Promise.all rejects as soon as any promise rejects, discarding the results of the ones that succeeded — Promise.allSettled waits for every promise to finish regardless of outcome and gives you a status per result, which is the right tool whenever partial success is meaningful (batch API calls where one bad ID shouldn’t blank out the other nine responses).
// Error handling with async/await — needs try/catch, unlike a .then() chain's .catch()async function safeFetch(id) { try { return await fetchUser(id); } catch (err) { console.error("Fetch failed:", err.message); return null; }}Closures
function makeCounter() { let count = 0; return { increment: () => ++count, reset: () => { count = 0; }, };}
const counter = makeCounter();counter.increment(); // 1counter.increment(); // 2counter.reset();A closure is a function that remembers the variables from the scope it was created in, even after that outer function has finished running — count here isn’t a global or a class field, it’s a variable that only the returned functions can see and modify, which is the mechanism behind private state in plain JavaScript (no class, no #private fields needed).
this — the rule that actually matters
const obj = { name: "Widget", regular() { return this.name; }, // regular function — `this` is dynamic arrow: () => { return this.name; }, // arrow — `this` is lexical (inherited from surrounding scope)};
obj.regular(); // "Widget" — called as obj.regular(), so `this` is objconst fn = obj.regular;fn(); // undefined (or throws in strict mode) — `this` depends on HOW it's called, not where it's defined
obj.arrow(); // whatever `this` was where obj was defined (often undefined/window) — NOT objRegular functions determine this from the call site (how the function was invoked), which is why detaching a method from its object (const fn = obj.method) and calling it separately silently breaks this. Arrow functions ignore the call site entirely and inherit this from their enclosing lexical scope at definition time — which is exactly why arrow functions are the standard choice for callbacks inside a class method (setTimeout(() => this.update(), 100)), where you want this to keep meaning the class instance, not whatever called the callback.
Optional chaining and nullish coalescing
const city = user?.address?.city; // undefined instead of throwing, if address is missingconst port = config.port ?? 8080; // 8080 only if port is null/undefined — NOT if it's 0 or ""const portWrong = config.port || 8080; // bug: replaces 0 or "" too, since both are falsyuser?.notify?.(); // optional call — only invokes if notify exists?? differs from || specifically around falsy-but-valid values: 0, "", and false are legitimate values that || would incorrectly override with the fallback, while ?? only falls back on null/undefined. A config port of 0 or an empty allowed string are the concrete cases where || silently produces the wrong result and ?? is the actual fix.
== vs === and type coercion
0 == false // true — coerced0 === false // false — different types, no coercion"" == 0 // true — both coerced to 0null == undefined // true — special-cased to equal each othernull === undefined // falseNaN === NaN // false — NaN is never equal to anything, including itselfNumber.isNaN(NaN) // true — the correct way to check for NaN== triggers type coercion following a specific, easy-to-misremember set of rules before comparing; === never coerces and compares type and value directly. The practical rule: use === by default, everywhere, and only reach for == in the one case people actually rely on it for — checking x == null as shorthand for “null or undefined.”
Classes and prototypal inheritance
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; }}
class Dog extends Animal { speak() { return `${super.speak()}, specifically a bark`; }}
new Dog("Rex").speak(); // "Rex makes a sound, specifically a bark"class syntax is sugar over JavaScript’s actual inheritance mechanism — the prototype chain. Every object has an internal link to another object (its prototype), and property lookups walk up that chain until they find a match or hit null. class/extends didn’t add a new inheritance model to the language; they made the existing prototype-chain model readable without manually wiring up Dog.prototype = Object.create(Animal.prototype), which is what this code compiled down to before ES6.
Common patterns
Debounce — collapse rapid-fire events into one
function debounce(fn, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); };}
const debouncedSearch = debounce((query) => fetchResults(query), 300);input.addEventListener("input", (e) => debouncedSearch(e.target.value));Every keystroke resets the timer — the search only actually fires once the user pauses for 300ms, instead of firing (and hitting the API) on every single keystroke.
Throttle — cap the rate, don’t collapse to the end
function throttle(fn, limit) { let waiting = false; return (...args) => { if (waiting) return; fn(...args); waiting = true; setTimeout(() => { waiting = false; }, limit); };}
window.addEventListener("scroll", throttle(() => updateScrollPosition(), 100));Debounce waits for silence; throttle guarantees a maximum firing rate regardless of how continuous the events are — scroll/resize handlers want throttle (you need periodic updates during the activity), search-as-you-type wants debounce (you only care about the final value).
Deep cloning without a library
const clone = structuredClone(original); // built into modern browsers/Node 17+, handles dates, Maps, Sets// Older environments: JSON.parse(JSON.stringify(original)) — but drops functions, undefined, and Dates become stringsMemoizing an expensive pure function
function memoize(fn) { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn(...args); cache.set(key, result); return result; };}Map and Set — when a plain object isn’t the right tool
const seen = new Set();const uniqueItems = items.filter(item => { if (seen.has(item.id)) return false; seen.add(item.id); return true;});
const cache = new Map();cache.set(userObject, "metadata"); // Map keys can be objects — plain {} keys are always coerced to stringscache.get(userObject);for (const [key, value] of cache) { ... } // directly iterable, unlike plain objectsA plain object silently stringifies any non-string key (obj[someObject] becomes obj["[object Object]"]), which breaks the moment two different objects get used as keys. Map keeps keys as their actual reference/type, and Set gives you O(1) uniqueness checks without building a lookup object by hand — both are the correct default over a plain object/array for these specific jobs, not just a stylistic preference.
Not covered: The DOM APIs, module bundler configuration, and TypeScript’s type system get their own dedicated coverage rather than a footnote here — this sheet is scoped to core language behavior. For deeper coverage, see the JavaScript guides.