================================================================================ JAVASCRIPT-CHEATSHEET.TXT ================================================================================ NAME javascript-cheatsheet.txt -- offline JavaScript / Node reference manual SYNOPSIS grep -A 40 "^SECTION_NAME" javascript-cheatsheet.txt less javascript-cheatsheet.txt grep -n "SECTION_NAME" javascript-cheatsheet.txt DESCRIPTION This file is a single flat plain-text reference manual covering the JavaScript language, built-in objects, arrays and collections, functional array methods, common interview / LeetCode-style algorithms and data structures, async programming and the event loop, the DOM, fetch, Node.js, npm, Express, JSON, file I/O, testing, debugging, design patterns, and general best practices. It is written to be grepped, not rendered. Every major topic has a large banner header made of '=' characters. Every subsection inside a topic has a smaller banner made of '-' characters. There is no markdown, no bullets unless natural, and no code fences. Code is always indented four spaces. Standard subsection names, greppable across the whole file: DESCRIPTION CREATE COMMON METHODS EXAMPLES COMMON LEETCODE PATTERNS TIME COMPLEXITY COMMON MISTAKES INTERVIEW NOTES TypeScript-specific material lives in typescript-cheatsheet.txt. Everything here is valid TypeScript too. SUGGESTED ALIAS Add something like this to your shell rc file: jsjs() { if [ -z "$1" ]; then less ~/javascript-cheatsheet.txt else grep -n -i --color=always "$*" ~/javascript-cheatsheet.txt | less -R fi } Usage: jsjs ARRAY jsjs CREATE jsjs COMMON METHODS jsjs "TREE DFS" jsjs ASYNC jsjs EVENT LOOP Since every section header is a unique all-caps banner line, grep -n will point you straight at the line number, and you can jump there directly with: less + ~/javascript-cheatsheet.txt TABLE OF CONTENTS GLOBALS AND BUILT-INS MATH SKELETON CODE NODE CLI NPM MODULES VARIABLES AND TYPES TYPE COERCION OPERATORS CONTROL FLOW FUNCTIONS CLOSURES THIS AND BINDING OBJECTS DESTRUCTURING AND SPREAD CLASSES PROTOTYPES ERROR HANDLING STRINGS ARRAY ARRAY HIGHER ORDER METHODS OBJECT AS HASHMAP MAP SET QUEUE STACK LINKED LIST HEAP BINARY SEARCH TREE DFS TREE BFS BACKTRACKING SLIDING WINDOW TWO POINTERS UNION FIND TRIE RECURSION AND MEMOIZATION DYNAMIC PROGRAMMING BIT MANIPULATION GRAPHS ITERATORS AND GENERATORS ASYNC EVENT LOOP REGEX JSON DATE AND TIME DOM EVENTS FETCH AND HTTP STORAGE NODE FILE IO EXPRESS TESTING COMMON ERRORS DEBUGGING DESIGN PATTERNS BEST PRACTICES LEETCODE PROBLEMS ================================================================================ GLOBALS AND BUILT-INS ================================================================================ DESCRIPTION JavaScript has no import statement for its own standard library. Every built-in below is available globally with no using/import directive, which is the single biggest structural difference from C#. ------------------------------------------------------------------------------- VALUE CONSTRUCTORS AND WRAPPERS ------------------------------------------------------------------------------- Object Array String Number Boolean BigInt Symbol Function ------------------------------------------------------------------------------- COLLECTIONS ------------------------------------------------------------------------------- Map Set WeakMap WeakSet ------------------------------------------------------------------------------- UTILITY NAMESPACES ------------------------------------------------------------------------------- Math JSON Date Intl Reflect Proxy ------------------------------------------------------------------------------- ASYNC ------------------------------------------------------------------------------- Promise queueMicrotask() setTimeout() setInterval() clearTimeout() clearInterval() AbortController ------------------------------------------------------------------------------- ERRORS ------------------------------------------------------------------------------- Error TypeError RangeError SyntaxError ReferenceError AggregateError ------------------------------------------------------------------------------- BINARY DATA ------------------------------------------------------------------------------- ArrayBuffer Uint8Array Int32Array Float64Array DataView TextEncoder TextDecoder ------------------------------------------------------------------------------- GLOBAL FUNCTIONS ------------------------------------------------------------------------------- parseInt() parseFloat() isNaN() isFinite() encodeURIComponent() decodeURIComponent() structuredClone() ------------------------------------------------------------------------------- BROWSER-ONLY GLOBALS ------------------------------------------------------------------------------- window document navigator location history fetch() localStorage sessionStorage console ------------------------------------------------------------------------------- NODE-ONLY GLOBALS ------------------------------------------------------------------------------- process process.env process.argv Buffer __dirname (CommonJS only) __filename (CommonJS only) globalThis require() (CommonJS only) ------------------------------------------------------------------------------- NODE STANDARD LIBRARY (MUST BE IMPORTED) ------------------------------------------------------------------------------- import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; import crypto from "node:crypto"; import http from "node:http"; import url from "node:url"; import readline from "node:readline/promises"; import { Worker } from "node:worker_threads"; ------------------------------------------------------------------------------- NOTES ------------------------------------------------------------------------------- - globalThis is the portable way to reach the global object in both browser and Node. - The "node:" prefix on core module imports is the modern convention and guarantees you get the built-in, not an npm package of the same name. - console exists in both browser and Node but is not part of the language spec; it is a host object. ================================================================================ MATH ================================================================================ DESCRIPTION The Math object is a namespace of static numeric helpers. Every JavaScript number is an IEEE 754 64-bit float; there is no separate int type. Integers are exact only up to Number.MAX_SAFE_INTEGER (2^53 - 1). ------------------------------------------------------------------------------- MINIMUM ------------------------------------------------------------------------------- Math.min(a, b); Math.min(...arr); // spread, careful with huge arrays ------------------------------------------------------------------------------- MAXIMUM ------------------------------------------------------------------------------- Math.max(a, b); Math.max(...arr); ------------------------------------------------------------------------------- ABSOLUTE VALUE ------------------------------------------------------------------------------- Math.abs(x); ------------------------------------------------------------------------------- POWER AND ROOTS ------------------------------------------------------------------------------- Math.pow(base, exp); base ** exp; // preferred Math.sqrt(x); Math.cbrt(x); Math.hypot(x, y); // sqrt(x*x + y*y) without overflow ------------------------------------------------------------------------------- ROUNDING ------------------------------------------------------------------------------- Math.round(x); // nearest, .5 rounds up (toward +Inf) Math.floor(x); // toward -Infinity Math.ceil(x); // toward +Infinity Math.trunc(x); // drop the fractional part Math.sign(x); // -1, 0, or 1 ------------------------------------------------------------------------------- LOGS AND EXPONENTIALS ------------------------------------------------------------------------------- Math.log(x); // natural log Math.log2(x); Math.log10(x); Math.exp(x); ------------------------------------------------------------------------------- TRIG ------------------------------------------------------------------------------- Math.sin(x); Math.cos(x); Math.tan(x); Math.asin(x); Math.acos(x); Math.atan(x); Math.atan2(y, x); ------------------------------------------------------------------------------- CONSTANTS ------------------------------------------------------------------------------- Math.PI Math.E Number.MAX_SAFE_INTEGER // 2^53 - 1 = 9007199254740991 Number.MIN_SAFE_INTEGER Number.MAX_VALUE Number.EPSILON Infinity -Infinity NaN ------------------------------------------------------------------------------- RANDOM ------------------------------------------------------------------------------- Math.random(); // [0, 1) Math.floor(Math.random() * n); // 0 .. n-1 Math.floor(Math.random() * (hi - lo + 1)) + lo; // lo .. hi inclusive Cryptographically secure: crypto.getRandomValues(new Uint32Array(1))[0]; crypto.randomUUID(); ------------------------------------------------------------------------------- INTEGER DIVISION AND MODULO ------------------------------------------------------------------------------- Math.trunc(a / b); // there is no integer division operator a % b; // sign follows the DIVIDEND, not divisor True mathematical modulo (always non-negative): const mod = (a, b) => ((a % b) + b) % b; mod(-1, 5); // 4 (-1 % 5 is -1 in JS) ------------------------------------------------------------------------------- FLOATING POINT COMPARISON ------------------------------------------------------------------------------- Math.abs(a - b) < Number.EPSILON; 0.1 + 0.2 === 0.3; // false (0.1 + 0.2).toFixed(2); // "0.30" ------------------------------------------------------------------------------- PARSING NUMBERS ------------------------------------------------------------------------------- Number("42"); // 42 Number(""); // 0 <-- gotcha Number("abc"); // NaN parseInt("42px", 10); // 42 stops at first non-digit parseFloat("3.14abc"); // 3.14 Number.parseInt("0x1F", 16); // 31 ------------------------------------------------------------------------------- CHECKING NUMBERS ------------------------------------------------------------------------------- Number.isInteger(x); Number.isFinite(x); // no coercion, prefer over isFinite() Number.isNaN(x); // no coercion, prefer over isNaN() Number.isSafeInteger(x); ------------------------------------------------------------------------------- FORMATTING ------------------------------------------------------------------------------- (1234.5678).toFixed(2); // "1234.57" (1234.5678).toPrecision(6); // "1234.57" (255).toString(2); // "11111111" binary (255).toString(16); // "ff" hex parseInt("ff", 16); // 255 (1234567.891).toLocaleString("en-US", { style: "currency", currency: "USD" }); // "$1,234,567.89" ------------------------------------------------------------------------------- BIGINT ------------------------------------------------------------------------------- const big = 9007199254740993n; big + 1n; BigInt(Number.MAX_SAFE_INTEGER); Cannot mix BigInt and Number in arithmetic without explicit conversion. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using Math.max() with no arguments returns -Infinity, and Math.min() returns Infinity. Reducing an empty array through spread silently produces these instead of throwing. Math.max(...arr) blows the call stack on very large arrays (roughly 100k+ elements). Use a reduce or a plain loop for big inputs. Assuming % is a modulo operator. It is a remainder operator; -1 % 5 is -1, not 4. Using isNaN() instead of Number.isNaN(). isNaN("abc") is true because it coerces first; Number.isNaN("abc") is false because the string is not the NaN value. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Be ready to explain why 0.1 + 0.2 !== 0.3 (IEEE 754 double precision, the same reason it happens in C# with double, and the same fix: compare within an epsilon or use a decimal/integer-cents representation). All JavaScript numbers are 64-bit floats. Integers are exact only up to Number.MAX_SAFE_INTEGER. Bitwise operators coerce to 32-bit signed integers, which is a common source of surprise on large values. ================================================================================ SKELETON CODE ================================================================================ DESCRIPTION Copy-paste starting points for a browser module, a Node ESM or CommonJS entry file, stdin-driven scripts, CLI argument parsing, and the function shape LeetCode expects. ------------------------------------------------------------------------------- BROWSER SCRIPT ------------------------------------------------------------------------------- // main.js const app = document.querySelector("#app"); app.textContent = "Hello, world"; ------------------------------------------------------------------------------- NODE ESM ENTRY POINT ------------------------------------------------------------------------------- // package.json must contain: { "type": "module" } // main.js async function main() { console.log("Hello, world"); } main().catch((err) => { console.error(err); process.exit(1); }); ------------------------------------------------------------------------------- NODE COMMONJS ENTRY POINT ------------------------------------------------------------------------------- // main.js const path = require("node:path"); function main() { console.log(path.basename(__filename)); } main(); ------------------------------------------------------------------------------- READING STDIN (COMPETITIVE / SCRIPTING) ------------------------------------------------------------------------------- import readline from "node:readline/promises"; const rl = readline.createInterface({ input: process.stdin }); for await (const line of rl) { console.log(line.trim()); } Read everything at once: import fs from "node:fs"; const input = fs.readFileSync(0, "utf8").trim().split("\n"); ------------------------------------------------------------------------------- COMMAND LINE ARGUMENTS ------------------------------------------------------------------------------- const args = process.argv.slice(2); // argv[0]=node, argv[1]=script Built-in parser (Node 18.3+): import { parseArgs } from "node:util"; const { values, positionals } = parseArgs({ options: { verbose: { type: "boolean", short: "v" }, out: { type: "string", short: "o" } }, allowPositionals: true }); ------------------------------------------------------------------------------- LEETCODE FUNCTION SHAPE ------------------------------------------------------------------------------- /** * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum = function (nums, target) { const seen = new Map(); for (let i = 0; i < nums.length; i++) { const need = target - nums[i]; if (seen.has(need)) return [seen.get(need), i]; seen.set(nums[i], i); } return []; }; LeetCode provides node shapes as: function ListNode(val, next) { this.val = val === undefined ? 0 : val; this.next = next === undefined ? null : next; } function TreeNode(val, left, right) { this.val = val === undefined ? 0 : val; this.left = left === undefined ? null : left; this.right = right === undefined ? null : right; } ================================================================================ NODE CLI ================================================================================ DESCRIPTION The node binary runs a file, a REPL, or an inline expression. Flags here cover watch mode, env files, the inspector, the built-in test runner, and common memory/stack knobs. ------------------------------------------------------------------------------- RUN A FILE ------------------------------------------------------------------------------- node main.js node --watch main.js // restart on change, Node 18.11+ node --env-file=.env main.js // load .env natively, Node 20.6+ ------------------------------------------------------------------------------- REPL ------------------------------------------------------------------------------- node node -e "console.log(process.version)" node -p "1 + 1" // -p prints the result ------------------------------------------------------------------------------- VERSION MANAGEMENT ------------------------------------------------------------------------------- node --version nvm install 22 nvm use 22 nvm alias default 22 echo "22" > .nvmrc ------------------------------------------------------------------------------- INSPECT AND DEBUG ------------------------------------------------------------------------------- node --inspect main.js // attach chrome://inspect or VS Code node --inspect-brk main.js // break on first line ------------------------------------------------------------------------------- BUILT-IN TEST RUNNER (NODE 18+) ------------------------------------------------------------------------------- node --test node --test --watch node --test --experimental-test-coverage ------------------------------------------------------------------------------- USEFUL FLAGS ------------------------------------------------------------------------------- --max-old-space-size=4096 // raise heap limit, in MB --trace-warnings // stack traces for warnings --stack-size=10000 // deeper recursion, use sparingly ================================================================================ NPM ================================================================================ DESCRIPTION npm is the default package manager for Node. The lockfile is the source of truth for CI; package.json scripts are how you run the project. ------------------------------------------------------------------------------- PROJECT SETUP ------------------------------------------------------------------------------- npm init -y npm install npm ci // clean, lockfile-exact, use in CI ------------------------------------------------------------------------------- INSTALLING ------------------------------------------------------------------------------- npm install express npm install -D vitest // devDependency npm install -g typescript // global npm install express@4.18.2 // exact version npm uninstall express ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- npm list --depth=0 npm outdated npm view express versions npm why lodash // explain why a package is installed ------------------------------------------------------------------------------- UPDATING AND AUDITING ------------------------------------------------------------------------------- npm update npm audit npm audit fix npm audit fix --force // may introduce breaking changes ------------------------------------------------------------------------------- SCRIPTS ------------------------------------------------------------------------------- "scripts": { "dev": "node --watch src/main.js", "start": "node src/main.js", "test": "vitest run", "lint": "eslint ." } npm run dev npm start // start/test/stop can skip "run" npm test ------------------------------------------------------------------------------- RUNNING WITHOUT INSTALLING ------------------------------------------------------------------------------- npx vitest npx create-vite@latest my-app ------------------------------------------------------------------------------- PUBLISHING ------------------------------------------------------------------------------- npm login npm version patch // bumps package.json + git tag npm publish --access public ------------------------------------------------------------------------------- SEMVER RANGES IN PACKAGE.JSON ------------------------------------------------------------------------------- "express": "^4.18.2" // 4.x.x, minor and patch updates allowed "express": "~4.18.2" // 4.18.x, patch updates only "express": "4.18.2" // exact "express": "*" // anything, do not do this ------------------------------------------------------------------------------- NOTES ------------------------------------------------------------------------------- - package-lock.json must be committed. It is what makes npm ci reproducible. - node_modules must not be committed. - npm ci deletes node_modules and installs exactly the lockfile; it is faster and safer than npm install in CI. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Installing a build-only tool as a regular dependency instead of a devDependency, which ships it to production and bloats the image. Running npm install in CI instead of npm ci, which can silently drift the dependency tree away from the lockfile. ================================================================================ MODULES ================================================================================ DESCRIPTION JavaScript has two module systems: ESM (import/export, the standard) and CommonJS (require/module.exports, the Node legacy). Knowing which one a file is in, and why, prevents most module errors. ------------------------------------------------------------------------------- ESM NAMED EXPORTS ------------------------------------------------------------------------------- // math.js export function add(a, b) { return a + b; } export const PI = 3.14159; // or export at the bottom function sub(a, b) { return a - b; } export { sub }; // main.js import { add, PI } from "./math.js"; import { add as sum } from "./math.js"; import * as math from "./math.js"; ------------------------------------------------------------------------------- ESM DEFAULT EXPORTS ------------------------------------------------------------------------------- // logger.js export default function log(msg) { console.log(msg); } // main.js import log from "./logger.js"; import log, { LEVELS } from "./logger.js"; // default + named ------------------------------------------------------------------------------- RE-EXPORTING (BARREL FILES) ------------------------------------------------------------------------------- // index.js export * from "./math.js"; export { default as log } from "./logger.js"; export * as math from "./math.js"; Barrel files are convenient but can defeat tree-shaking and create import cycles in large apps. Use them deliberately. ------------------------------------------------------------------------------- DYNAMIC IMPORT ------------------------------------------------------------------------------- const { default: heavy } = await import("./heavy.js"); Returns a promise. Works in both ESM and CommonJS, and is the main mechanism for code splitting and lazy loading. ------------------------------------------------------------------------------- COMMONJS ------------------------------------------------------------------------------- // math.js function add(a, b) { return a + b; } module.exports = { add }; module.exports.PI = 3.14159; // main.js const { add } = require("./math"); const math = require("./math"); ------------------------------------------------------------------------------- CHOOSING BETWEEN THEM ------------------------------------------------------------------------------- package.json "type": "module" -> .js files are ESM package.json "type": "commonjs" -> .js files are CommonJS (default) .mjs is always ESM regardless of "type" .cjs is always CommonJS regardless of "type" ESM can import CommonJS (default import only, in most cases). CommonJS cannot require ESM; it must use dynamic import(). ------------------------------------------------------------------------------- __DIRNAME IN ESM ------------------------------------------------------------------------------- import { fileURLToPath } from "node:url"; import path from "node:path"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); Node 20.11+ provides it directly: const __dirname = import.meta.dirname; ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Omitting the file extension in a relative ESM import. Node requires "./math.js", not "./math". Bundlers allow the shorter form, which makes the code non-portable to plain Node. ERR_REQUIRE_ESM: requiring a package that is ESM-only from a CommonJS file. Fix by converting the caller to ESM or using await import(). "Cannot use import statement outside a module": the file is being treated as CommonJS. Add "type": "module" or rename to .mjs. Circular imports. In ESM they resolve to a partially-initialized module rather than throwing, which produces confusing undefined values at import time rather than a clear error. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- ESM imports are statically analyzable and hoisted, which is what makes tree-shaking possible. CommonJS require() is a runtime function call, so bundlers cannot reliably eliminate dead code from it. That is the core reason the ecosystem moved to ESM. ================================================================================ VARIABLES AND TYPES ================================================================================ DESCRIPTION JavaScript is dynamically typed with seven primitive types plus objects. Variables are declared with const, let, or (never) var. ------------------------------------------------------------------------------- DECLARATIONS ------------------------------------------------------------------------------- const name = "Jake"; // block scoped, cannot be reassigned let count = 0; // block scoped, reassignable var old = 1; // function scoped, hoisted, avoid const does not mean immutable. It means the binding cannot be reassigned. The object it points at is still mutable: const user = { name: "Jake" }; user.name = "J"; // fine user = {}; // TypeError: Assignment to constant variable Default to const. Use let only when you actually reassign. ------------------------------------------------------------------------------- PRIMITIVE TYPES ------------------------------------------------------------------------------- string "hello" 'hello' `hello ${name}` number 42 3.14 -0 Infinity NaN bigint 9007199254740993n boolean true false undefined declared but not assigned null explicitly "no value" symbol Symbol("id") Symbol.for("global") Everything else is an object, including arrays and functions. ------------------------------------------------------------------------------- TYPEOF ------------------------------------------------------------------------------- typeof "s" // "string" typeof 1 // "number" typeof 1n // "bigint" typeof true // "boolean" typeof undefined // "undefined" typeof Symbol() // "symbol" typeof {} // "object" typeof [] // "object" <-- not "array" typeof null // "object" <-- historical bug, never fixed typeof function(){} // "function" Reliable type checks: Array.isArray(x) x === null Object.prototype.toString.call(x) // "[object Date]" etc. x instanceof Date ------------------------------------------------------------------------------- NULL VS UNDEFINED ------------------------------------------------------------------------------- undefined the language's "not set": unassigned variables, missing object properties, missing function arguments, the return value of a function with no return. null the programmer's "intentionally empty". Practical rule: never assign undefined yourself. Use null when you mean "deliberately nothing", and let undefined mean "never set". undefined == null // true (loose) undefined === null // false (strict) ------------------------------------------------------------------------------- TEMPLATE LITERALS ------------------------------------------------------------------------------- const msg = `Hello, ${name}! You have ${count + 1} items.`; const multiline = ` line one line two `; Tagged templates: function sql(strings, ...values) { return strings.raw.join("?"); } sql`SELECT * FROM users WHERE id = ${id}`; ------------------------------------------------------------------------------- SCOPE AND HOISTING ------------------------------------------------------------------------------- var declarations are hoisted and initialized to undefined. let and const are hoisted but sit in the "temporal dead zone" until the declaration line executes; touching them before that throws. console.log(a); // undefined var a = 1; console.log(b); // ReferenceError: Cannot access 'b' before init let b = 1; Function declarations are fully hoisted; function expressions are not. hoisted(); // works function hoisted() {} notHoisted(); // TypeError: notHoisted is not a function var notHoisted = function () {}; ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using var in a loop with a closure. All iterations share one binding: for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3 for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 0 1 2 Expecting const to deep-freeze an object. Use Object.freeze() for a shallow freeze, or structuredClone() to copy before mutating. Forgetting that an undeclared assignment in non-strict mode creates a global. ESM is always strict mode, which makes this a ReferenceError. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- "Explain the temporal dead zone" and "why is typeof null 'object'" are both extremely common. The second is a bug from the original 1995 implementation where the type tag for objects was 0 and null was the null pointer, also 0; fixing it would break the web. ================================================================================ TYPE COERCION ================================================================================ DESCRIPTION Coercion is where most JavaScript surprises live. The defense is simple: use === always, and convert explicitly. ------------------------------------------------------------------------------- FALSY VALUES ------------------------------------------------------------------------------- There are exactly eight falsy values: false 0 -0 0n "" null undefined NaN Everything else is truthy, including: [] // empty array is truthy {} // empty object is truthy "0" // non-empty string is truthy "false" // non-empty string is truthy function(){} ------------------------------------------------------------------------------- EQUALITY ------------------------------------------------------------------------------- === strict, no coercion. Use this. == loose, coerces. Avoid except for the null/undefined idiom. 0 == ""; // true 0 == "0"; // true "" == "0"; // false <-- not transitive null == undefined; // true null == 0; // false NaN == NaN; // false [] == false; // true [1] == 1; // true The one defensible use of ==: if (x == null) { } // true for both null and undefined Object.is() is like === with two differences: Object.is(NaN, NaN); // true Object.is(0, -0); // false NaN === NaN; // false 0 === -0; // true ------------------------------------------------------------------------------- EXPLICIT CONVERSION ------------------------------------------------------------------------------- To string: String(x) x.toString() // throws on null/undefined `${x}` To number: Number(x) parseInt(x, 10) parseFloat(x) +x // unary plus, terse but obscure To boolean: Boolean(x) !!x ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Guarding with a truthiness check when 0 or "" are valid values: if (count) { } // skips count === 0 if (count !== undefined) { } Using || for defaults when 0 or "" should be kept: const limit = input || 10; // 0 becomes 10 const limit = input ?? 10; // 0 stays 0 Comparing objects with ===. It compares references, not contents: { a: 1 } === { a: 1 }; // false JSON.stringify(a) === JSON.stringify(b); // fragile, key order ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Be able to explain why [] == false is true: [] converts to "" via toPrimitive, false converts to 0, "" converts to 0, and 0 == 0. The point of the story is not the trivia; it is that the abstract equality algorithm has enough steps that nobody should rely on it. ================================================================================ OPERATORS ================================================================================ DESCRIPTION JavaScript operators cover arithmetic, assignment, comparison, logical short-circuiting, nullish coalescing, optional chaining, and bitwise work. Several of them coerce types; that is the usual source of bugs. ------------------------------------------------------------------------------- ARITHMETIC ------------------------------------------------------------------------------- a + b // addition, or string concatenation if either is a string a - b a * b a / b // always float division, 7 / 2 === 3.5 a % b // remainder, sign follows the dividend a ** b // exponent a++ ++a a-- --a ------------------------------------------------------------------------------- ASSIGNMENT ------------------------------------------------------------------------------- a += b a -= b a *= b a /= b a %= b a **= b a ||= b // assign if a is falsy a &&= b // assign if a is truthy a ??= b // assign if a is null or undefined ------------------------------------------------------------------------------- COMPARISON ------------------------------------------------------------------------------- === !== strict equality, no coercion == != loose equality, coerces < > <= >= ------------------------------------------------------------------------------- LOGICAL ------------------------------------------------------------------------------- a && b // returns a if falsy, else b a || b // returns a if truthy, else b !a a ?? b // returns a unless a is null/undefined && and || return operands, not booleans. This is what makes short-circuit rendering work: isLoggedIn && renderDashboard() ------------------------------------------------------------------------------- NULLISH AND OPTIONAL CHAINING ------------------------------------------------------------------------------- user?.profile?.email user?.getName?.() arr?.[0] const email = user?.profile?.email ?? "none"; Optional chaining short-circuits the entire chain on null/undefined and evaluates to undefined rather than throwing. ------------------------------------------------------------------------------- BITWISE ------------------------------------------------------------------------------- a & b AND a | b OR a ^ b XOR ~a NOT a << b left shift a >> b signed right shift a >>> b unsigned right shift (JS-specific, no C# equivalent for int) Bitwise operators coerce operands to 32-bit signed integers, so they silently truncate anything above 2^31 - 1. ------------------------------------------------------------------------------- OTHER OPERATORS ------------------------------------------------------------------------------- typeof x x instanceof Ctor "key" in obj // own or inherited property delete obj.key void 0 // undefined a, b // comma operator, evaluates both, yields b condition ? ifTrue : ifFalse ...spread / ...rest ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- const isEven = (n & 1) === 0; const doubled = n << 1; const halved = n >> 1; const swap = (a, b) => [b, a]; const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi); const label = age < 13 ? "child" : age < 20 ? "teenager" : age < 65 ? "adult" : "senior"; ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- "1" + 1 is "11" but "1" - 1 is 0. Plus is overloaded for strings; minus is not. Mixing ?? with || or && without parentheses is a SyntaxError. This is deliberate, to force you to be explicit: a ?? b || c // SyntaxError (a ?? b) || c // fine Using bitwise | 0 as a floor. It truncates toward zero and breaks above 2^31. Math.trunc() is clearer and correct. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- The difference between || and ?? comes up constantly in React and config-handling code. || falls back on any falsy value; ?? falls back only on null and undefined. Getting this wrong is a real bug class when 0, "", or false are legitimate values. ================================================================================ CONTROL FLOW ================================================================================ DESCRIPTION if, switch, and the loop forms. Prefer for-of for values, for-in only for keys on objects you own, and an object/map lookup instead of a long switch when the cases are data. ------------------------------------------------------------------------------- IF / ELSE ------------------------------------------------------------------------------- if (score >= 90) { grade = "A"; } else if (score >= 80) { grade = "B"; } else { grade = "F"; } ------------------------------------------------------------------------------- SWITCH ------------------------------------------------------------------------------- switch (day) { case "Saturday": case "Sunday": isWeekend = true; break; default: isWeekend = false; } switch uses === for matching, so no type coercion happens. Fallthrough is allowed and silent, unlike C#. Always break or return. Block-scope a case that declares variables: case "a": { const x = 1; break; } ------------------------------------------------------------------------------- OBJECT LOOKUP INSTEAD OF SWITCH ------------------------------------------------------------------------------- const handlers = { create: () => doCreate(), update: () => doUpdate(), delete: () => doDelete() }; (handlers[action] ?? (() => { throw new Error("unknown") }))(); This is the idiomatic JavaScript replacement for a C# switch expression, and it is easier to extend and test. ------------------------------------------------------------------------------- LOOPS ------------------------------------------------------------------------------- for (let i = 0; i < 10; i++) { } for (const item of array) { } // values, works on iterables for (const key in object) { } // keys, includes inherited for (const [k, v] of Object.entries(o)) { } for (const [k, v] of map) { } let i = 0; while (i < 10) { i++; } let j = 0; do { j++; } while (j < 10); for await (const chunk of stream) { } // async iterables Loop control: break exits the nearest loop or switch continue skips to next iteration outer: for (...) labels, break outer / continue outer ------------------------------------------------------------------------------- FOR-IN VS FOR-OF ------------------------------------------------------------------------------- const arr = ["a", "b"]; for (const i of arr) console.log(i); // "a", "b" for (const i in arr) console.log(i); // "0", "1" <-- string keys for-in iterates enumerable string keys including inherited ones, in an unreliable order for integer-like keys. Never use it on arrays. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using for-in on an array, which yields index strings and may pick up properties added to Array.prototype by a badly behaved library. Using forEach when you need to break out early. forEach cannot be stopped; use for-of, or some()/every()/find() which short-circuit. Using await inside forEach. The callback is async but forEach does not await it, so the loop finishes before any work does. Use for-of with await, or Promise.all(arr.map(...)) for parallelism. Mutating an array while iterating it with for-of, which silently skips elements as indices shift. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- "How do you exit a forEach early" is a small trap. The answer is that you cannot; the honest follow-up is naming for-of, some(), or find() as the right tools depending on intent. ================================================================================ FUNCTIONS ================================================================================ DESCRIPTION Functions are values. Declaration, expression, arrow, default/rest/destructured parameters, and the differences that matter for this and arguments all live here. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- function add(a, b) { return a + b; } // declaration, hoisted const add = function (a, b) { return a + b; }; // expression const add = (a, b) => a + b; // arrow, implicit return const add = (a, b) => { return a + b; }; // arrow, block body const makeObj = () => ({ a: 1 }); // parens to return object (function () { })(); // IIFE (async () => { })(); // async IIFE ------------------------------------------------------------------------------- DEFAULT PARAMETERS ------------------------------------------------------------------------------- function greet(name, greeting = "Hello") { return `${greeting}, ${name}!`; } Defaults apply only for undefined, not null: greet("Jake", null); // "null, Jake!" Later defaults can reference earlier parameters: function rect(w, h = w) { return w * h; } ------------------------------------------------------------------------------- REST PARAMETERS ------------------------------------------------------------------------------- function sum(...numbers) { return numbers.reduce((acc, n) => acc + n, 0); } sum(1, 2, 3); // 6 sum(...[1, 2, 3]); // 6 Rest must be the last parameter. Unlike C# params, it is a real array with all array methods available. ------------------------------------------------------------------------------- DESTRUCTURED PARAMETERS ------------------------------------------------------------------------------- function createUser({ name, age = 0, isAdmin = false } = {}) { return { name, age, isAdmin }; } createUser({ name: "Jake", isAdmin: true }); This is the JavaScript equivalent of named arguments, and the standard way to write a function with more than about three parameters. ------------------------------------------------------------------------------- ARROW VS FUNCTION ------------------------------------------------------------------------------- Arrow functions: - do not have their own this; they close over the enclosing this - do not have arguments - cannot be used as constructors (no new) - have no prototype property - cannot be generators Use arrows for callbacks and short helpers. Use function for object methods that need this, and for anything called with new. ------------------------------------------------------------------------------- FIRST CLASS FUNCTIONS ------------------------------------------------------------------------------- const ops = { add: (a, b) => a + b }; const apply = (fn, ...args) => fn(...args); function once(fn) { let called = false, result; return (...args) => { if (!called) { called = true; result = fn(...args); } return result; }; } const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x); const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x); ------------------------------------------------------------------------------- DEBOUNCE AND THROTTLE ------------------------------------------------------------------------------- function debounce(fn, ms) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; } function throttle(fn, ms) { let last = 0; return (...args) => { const now = Date.now(); if (now - last >= ms) { last = now; fn(...args); } }; } Debounce: run once after activity stops (search-as-you-type). Throttle: run at most once per interval (scroll, resize). These two are among the most common live-coding interview asks. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using an arrow function as an object method and expecting this to be the object. It is not; it is whatever this was at definition time. Relying on arguments inside an arrow function. Use rest parameters. Forgetting that default parameters are evaluated at call time, so a default of [] or {} creates a fresh one per call (which is usually what you want, and the opposite of the Python gotcha). ================================================================================ CLOSURES ================================================================================ DESCRIPTION A closure is a function that retains access to the variables of the scope in which it was defined, even after that scope has returned. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- function counter() { let count = 0; return { increment: () => ++count, get value() { return count; } }; } const c = counter(); c.increment(); c.value; // 1 count is genuinely private. There is no way to reach it from outside. ------------------------------------------------------------------------------- PRACTICAL USES ------------------------------------------------------------------------------- Private state (the module pattern, and the basis of React hooks). Memoization caches. Partial application and currying. Event handlers that need to remember setup values. const add = (a) => (b) => a + b; const add5 = add(5); add5(3); // 8 ------------------------------------------------------------------------------- THE LOOP CLOSURE PROBLEM ------------------------------------------------------------------------------- for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 3 3 3 -- one shared binding for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 0 1 2 -- let creates a new binding per iteration The pre-ES6 fix, still worth recognizing: for (var i = 0; i < 3; i++) { (function (j) { setTimeout(() => console.log(j), 0); })(i); } ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Accidental memory retention: a closure that captures a large object keeps it alive for as long as the closure is reachable. Long-lived event listeners are the usual culprit; remove them when done. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- "What is a closure" is asked in nearly every JavaScript interview. The strong answer is not the definition but a concrete use: private state, a memoize function, or explaining why React's useState works at all. ================================================================================ THIS AND BINDING ================================================================================ DESCRIPTION In JavaScript, this is determined by how a function is CALLED, not where it is defined. Arrow functions are the exception: they capture this lexically and ignore the call site. ------------------------------------------------------------------------------- THE FOUR BINDING RULES ------------------------------------------------------------------------------- 1. new binding new Foo() this is the new object 2. explicit binding fn.call(obj) this is obj 3. implicit binding obj.fn() this is obj 4. default binding fn() undefined in strict mode, globalThis otherwise Arrow functions bypass all four and use the enclosing scope's this. ------------------------------------------------------------------------------- LOSING THIS ------------------------------------------------------------------------------- const user = { name: "Jake", greet() { return `Hi, ${this.name}`; } }; user.greet(); // "Hi, Jake" const fn = user.greet; fn(); // TypeError, this is undefined setTimeout(user.greet, 100); // same problem Fixes: setTimeout(() => user.greet(), 100); setTimeout(user.greet.bind(user), 100); ------------------------------------------------------------------------------- CALL, APPLY, BIND ------------------------------------------------------------------------------- fn.call(thisArg, a, b); // args listed fn.apply(thisArg, [a, b]); // args as array const bound = fn.bind(thisArg); // returns a new permanently bound fn bind can also partially apply: const add = (a, b) => a + b; const add5 = add.bind(null, 5); ------------------------------------------------------------------------------- CLASS METHOD BINDING ------------------------------------------------------------------------------- class Button { label = "Click"; handleClick = () => console.log(this.label); // class field, bound handleOther() { console.log(this.label); } // not bound } Class fields holding arrow functions are the modern fix for passing methods as callbacks. The cost is one function per instance rather than one shared on the prototype. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Passing a method reference to an event listener or setTimeout without binding it, producing "Cannot read properties of undefined". Using an arrow function for a prototype method that needs this to be the instance. Assuming bind() mutates. It returns a new function; the original is unchanged. ================================================================================ OBJECTS ================================================================================ DESCRIPTION Plain objects are string/symbol keyed maps with a prototype. They are the default record type; use Map when keys are dynamic, non-string, or insertion-order iteration must be exact. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- const user = { name: "Jake", age: 30, greet() { return `Hi, ${this.name}`; }, get initials() { return this.name[0]; }, set nickname(v) { this.name = v; }, ["dynamic" + "Key"]: true }; const shorthand = { name, age }; // { name: name, age: age } Object.create(protoObj); Object.create(null); // no prototype, pure dict ------------------------------------------------------------------------------- ACCESSING ------------------------------------------------------------------------------- user.name user["name"] user[key] // dynamic user?.profile?.email delete user.age ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- Object.keys(obj) // own enumerable string keys Object.values(obj) Object.entries(obj) // [[k, v], ...] Object.fromEntries(pairs) // inverse of entries Object.assign(target, ...sources) // shallow merge, mutates target Object.freeze(obj) // shallow immutability Object.isFrozen(obj) Object.hasOwn(obj, key) // modern, prefer over hasOwnProperty Object.getPrototypeOf(obj) Object.defineProperty(obj, key, descriptor) Object.getOwnPropertyNames(obj) // includes non-enumerable structuredClone(obj) // true deep clone ------------------------------------------------------------------------------- COPYING ------------------------------------------------------------------------------- Shallow: const copy = { ...original }; const copy = Object.assign({}, original); Deep: const deep = structuredClone(original); // handles Map, Set, // Date, cycles const deep = JSON.parse(JSON.stringify(o)); // loses Date, Map, // undefined, functions ------------------------------------------------------------------------------- MERGING ------------------------------------------------------------------------------- const merged = { ...defaults, ...overrides }; Later sources win. This is shallow: nested objects are replaced wholesale, not merged. ------------------------------------------------------------------------------- ITERATING ------------------------------------------------------------------------------- for (const [key, value] of Object.entries(obj)) { } Object.keys(obj).forEach((k) => { }); Key order: integer-like keys first in ascending order, then string keys in insertion order, then symbols. Do not rely on this for logic. ------------------------------------------------------------------------------- TRANSFORMING ------------------------------------------------------------------------------- Map values: Object.fromEntries( Object.entries(obj).map(([k, v]) => [k, v * 2]) ); Filter keys: Object.fromEntries( Object.entries(obj).filter(([k, v]) => v != null) ); Pick and omit: const pick = (o, keys) => Object.fromEntries(keys.filter((k) => k in o).map((k) => [k, o[k]])); const omit = (o, keys) => Object.fromEntries( Object.entries(o).filter(([k]) => !keys.includes(k)) ); ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Treating spread as a deep copy. Nested objects are shared by reference between the copy and the original. Using JSON round-tripping as a deep clone on data containing Date objects, Map, Set, undefined, or functions. structuredClone handles all but functions. Using an object as a keyed collection when keys are dynamic or non-string. Use Map instead; see the MAP section. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Be ready to write a deep clone by hand with recursion, handling arrays, plain objects, and a cycle-tracking WeakMap. It is a classic mid-level JavaScript question. ================================================================================ DESTRUCTURING AND SPREAD ================================================================================ DESCRIPTION Destructuring unpacks arrays and objects into bindings. Spread copies enumerable own properties or iterable elements. Both are shallow. ------------------------------------------------------------------------------- ARRAY DESTRUCTURING ------------------------------------------------------------------------------- const [a, b] = [1, 2]; const [a, , c] = [1, 2, 3]; // skip const [a, ...rest] = [1, 2, 3]; // rest = [2, 3] const [a = 10] = []; // default [a, b] = [b, a]; // swap ------------------------------------------------------------------------------- OBJECT DESTRUCTURING ------------------------------------------------------------------------------- const { name, age } = user; const { name: userName } = user; // rename const { age = 0 } = user; // default const { name: n = "anon" } = user; // rename + default const { profile: { email } = {} } = user; // nested, guarded const { name, ...others } = user; // rest In a for-of: for (const { id, title } of posts) { } From a function return: const { data, error } = await load(); ------------------------------------------------------------------------------- SPREAD ------------------------------------------------------------------------------- const merged = [...a, ...b]; const copy = [...arr]; const chars = [..."hello"]; // any iterable const fromSet = [...new Set(arr)]; // dedupe idiom const obj = { ...defaults, ...opts }; Math.max(...nums); ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Destructuring from undefined throws. Give the source a default: function f({ a } = {}) { } Forgetting that rest in object destructuring must come last. Using spread in a loop to build an array, which is O(n^2). Push instead, or use flat()/concat() once at the end. ================================================================================ CLASSES ================================================================================ DESCRIPTION class is the modern syntax for constructor functions plus a prototype. Fields, privacy, inheritance, and static initialization are real language features, not purely sugar. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- class User { static count = 0; // static field #password; // truly private field name; // public field constructor(name, password) { this.name = name; this.#password = password; User.count++; } greet() { return `Hi, ${this.name}`; } get displayName() { return this.name.toUpperCase(); } set displayName(v) { this.name = v.toLowerCase(); } static create(name) { return new User(name, ""); } #hash() { return this.#password.length; } // private method toString() { return `User(${this.name})`; } } ------------------------------------------------------------------------------- INHERITANCE ------------------------------------------------------------------------------- class Admin extends User { constructor(name, password, level) { super(name, password); // must come before any use of this this.level = level; } greet() { return super.greet() + " (admin)"; } } admin instanceof Admin; // true admin instanceof User; // true ------------------------------------------------------------------------------- PRIVATE FIELDS ------------------------------------------------------------------------------- #field is enforced by the language, not a convention. Accessing it from outside the class body is a SyntaxError, not undefined. class C { #x = 1; static has(o) { return #x in o; } } The older convention of a leading underscore (_x) is purely advisory. ------------------------------------------------------------------------------- STATIC BLOCKS ------------------------------------------------------------------------------- class Config { static settings; static { Config.settings = loadDefaults(); } } ------------------------------------------------------------------------------- ABSTRACT-ISH CLASSES ------------------------------------------------------------------------------- JavaScript has no abstract or interface keyword. The conventional approximations: class Shape { constructor() { if (new.target === Shape) { throw new Error("Shape is abstract"); } } area() { throw new Error("not implemented"); } } For real interfaces, use TypeScript. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using this before calling super() in a derived constructor, which throws a ReferenceError. Assuming class methods are bound. They are not; extracting one loses this. See THIS AND BINDING. Expecting classes to be hoisted like function declarations. They are in the temporal dead zone; you cannot instantiate before the declaration. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Classes in JavaScript are syntactic sugar over prototypes, but not purely: private fields, the new.target check, and the requirement to call super() have no clean prototype equivalent. Saying "just sugar" without that caveat invites a follow-up. ================================================================================ PROTOTYPES ================================================================================ DESCRIPTION Every object has an internal link to another object, its prototype. Property lookup walks that chain until it hits null. This is JavaScript's inheritance mechanism; classes are a layer on top. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- const arr = []; arr -> Array.prototype -> Object.prototype -> null Object.getPrototypeOf(arr) === Array.prototype; // true Object.getPrototypeOf(Array.prototype) === Object.prototype; ------------------------------------------------------------------------------- CONSTRUCTOR FUNCTIONS (PRE-CLASS STYLE) ------------------------------------------------------------------------------- function User(name) { this.name = name; } User.prototype.greet = function () { return `Hi, ${this.name}`; }; const u = new User("Jake"); What new does: 1. creates a new empty object 2. sets its prototype to User.prototype 3. calls User with this bound to the new object 4. returns it, unless the constructor returns another object ------------------------------------------------------------------------------- CHECKING AND SETTING ------------------------------------------------------------------------------- Object.getPrototypeOf(obj) Object.setPrototypeOf(obj, proto) // slow, avoid in hot paths obj.__proto__ // legacy accessor, avoid Ctor.prototype.isPrototypeOf(obj) obj instanceof Ctor Object.hasOwn(obj, "key") // own, not inherited ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Extending built-in prototypes (Array.prototype.myHelper = ...). It is global, collides with future language features, and breaks for-in loops across the whole program. Confusing __proto__ (the link on an instance) with .prototype (the property on a constructor function used to build that link). ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- "Explain prototypal inheritance" and "what does new do" are staple questions. Walking through the four steps of new is the cleanest way to answer both at once. ================================================================================ ERROR HANDLING ================================================================================ DESCRIPTION try/catch/finally, throwing Error objects, custom subclasses, async rejection, and the Result pattern for expected failures. Always throw Error, never a string. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- try { risky(); } catch (err) { console.error(err.message); } finally { cleanup(); } Optional catch binding when you do not need the error: try { risky(); } catch { fallback(); } ------------------------------------------------------------------------------- THROWING ------------------------------------------------------------------------------- throw new Error("something broke"); throw new TypeError("expected a string"); You can throw anything, but always throw an Error. Only Error carries a stack trace. ------------------------------------------------------------------------------- CUSTOM ERRORS ------------------------------------------------------------------------------- class ValidationError extends Error { constructor(message, field) { super(message); this.name = "ValidationError"; this.field = field; } } try { throw new ValidationError("required", "email"); } catch (err) { if (err instanceof ValidationError) console.log(err.field); else throw err; // rethrow what you cannot handle } ------------------------------------------------------------------------------- ERROR CAUSE ------------------------------------------------------------------------------- try { await db.query(); } catch (err) { throw new Error("Failed to load user", { cause: err }); } Preserves the original error, equivalent to an inner exception. ------------------------------------------------------------------------------- BUILT-IN ERROR TYPES ------------------------------------------------------------------------------- Error base TypeError wrong type, calling a non-function, null property RangeError value out of range, invalid array length ReferenceError undeclared variable, TDZ access SyntaxError invalid syntax, JSON.parse failure AggregateError multiple errors, from Promise.any ------------------------------------------------------------------------------- ASYNC ERRORS ------------------------------------------------------------------------------- try { const data = await fetchUser(); } catch (err) { handle(err); } Promise style: fetchUser().then(use).catch(handle).finally(done); Global handlers: // browser window.addEventListener("unhandledrejection", (e) => { }); window.addEventListener("error", (e) => { }); // node process.on("unhandledRejection", (reason) => { }); process.on("uncaughtException", (err) => { process.exit(1); }); ------------------------------------------------------------------------------- RESULT PATTERN ------------------------------------------------------------------------------- Where exceptions are awkward (expected failure paths): async function safeFetch(url) { try { const res = await fetch(url); if (!res.ok) return { ok: false, error: res.status }; return { ok: true, data: await res.json() }; } catch (err) { return { ok: false, error: err }; } } ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Forgetting that fetch() does not reject on 4xx/5xx. Only a network failure rejects. You must check res.ok yourself. An unawaited async call inside try/catch. The promise rejects after the try block has already exited, so the catch never fires. Swallowing errors with an empty catch block, or catching and logging without rethrowing when the caller needed to know. Returning from inside try when finally also returns; the finally return wins and silently discards the try value. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Be ready to explain why an error thrown inside a setTimeout callback cannot be caught by a try/catch wrapping the setTimeout call: the callback runs on a later tick, on a different stack. ================================================================================ STRINGS ================================================================================ DESCRIPTION Strings are immutable UTF-16 sequences. Every "mutating" method returns a new string. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- const a = "double"; const b = 'single'; const c = `template ${value}`; const d = String(42); const e = "ab".repeat(3); // "ababab" ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- s.length s[0] s.charAt(0) s.at(-1) // last character, negative index supported s.charCodeAt(0) // UTF-16 code unit s.codePointAt(0) // full code point String.fromCharCode(65) // "A" String.fromCodePoint(0x1f600) s.includes("x") s.startsWith("x") s.endsWith("x") s.indexOf("x") // -1 if absent s.lastIndexOf("x") s.search(/re/) // index or -1 s.match(/re/g) // array of matches or null s.matchAll(/re/g) // iterator of full match objects s.slice(2, 5) // supports negative indices, preferred s.slice(-3) // last three characters s.substring(2, 5) // no negatives, swaps args if reversed s.split("") // array of characters (breaks on emoji) [...s] // array of code points (emoji-safe) s.split(",") s.split(/\s+/) s.toUpperCase() s.toLowerCase() s.trim() s.trimStart() s.trimEnd() s.padStart(5, "0") // "00042" s.padEnd(5, ".") s.replace("a", "b") // first occurrence only s.replaceAll("a", "b") s.replace(/a/g, "b") s.concat(other) // prefer + or template literals s.normalize("NFC") // unicode normalization s.localeCompare(other) // -1 / 0 / 1, locale aware sort ------------------------------------------------------------------------------- REPLACE WITH A FUNCTION ------------------------------------------------------------------------------- "a1b2".replace(/\d/g, (m) => Number(m) * 2); // "a2b4" "hello world".replace(/\b\w/g, (c) => c.toUpperCase()); ------------------------------------------------------------------------------- BUILDING STRINGS IN LOOPS ------------------------------------------------------------------------------- There is no StringBuilder. Modern engines optimize += with ropes, so it is fine for most cases. For very hot paths, push to an array and join once: const parts = []; for (const x of items) parts.push(String(x)); const out = parts.join(""); ------------------------------------------------------------------------------- COMMON LEETCODE PATTERNS ------------------------------------------------------------------------------- Reverse: [...s].reverse().join("") Is palindrome: s === [...s].reverse().join("") Char frequency: const f = {}; for (const c of s) f[c] = (f[c] ?? 0) + 1; Capitalize: s[0].toUpperCase() + s.slice(1) Title case: s.replace(/\b\w/g, (c) => c.toUpperCase()) Count occurrences: [...s.matchAll(/x/g)].length Strip non-alnum: s.replace(/[^a-z0-9]/gi, "") Slug: s.toLowerCase().trim().replace(/[^\w]+/g, "-") Compare ignoring case: a.toLowerCase() === b.toLowerCase() Is numeric: !isNaN(Number(s)) && s.trim() !== "" ------------------------------------------------------------------------------- UNICODE PITFALL ------------------------------------------------------------------------------- "".length // 2, it is a surrogate pair "".split("").length // 2, broken halves [..."ee"].length // correct code point count For grapheme clusters (emoji with modifiers, combined characters): const seg = new Intl.Segmenter("en", { granularity: "grapheme" }); [...seg.segment(s)].length ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Expecting replace() to replace all occurrences. It replaces the first unless given a /g regex. Use replaceAll(). Using split("") on text containing emoji or non-BMP characters, which splits surrogate pairs into meaningless halves. Using substring() with negative numbers. It treats them as 0. slice() is almost always the right choice. Comparing strings with < and > for sorting non-ASCII text. Use localeCompare or Intl.Collator. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- String immutability means naive concatenation in a loop is O(n^2) in theory. Engines optimize this heavily in practice, but saying "array + join, or accept the rope optimization" shows you know the trade-off. ================================================================================ ARRAY ================================================================================ DESCRIPTION A dense, ordered, dynamically sized list. Arrays are the default collection in JavaScript. They are objects, they are mutable, and most interview problems start here. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- const a = [1, 2, 3]; const b = new Array(5); // 5 empty slots, avoid const c = new Array(5).fill(0); // [0,0,0,0,0] const d = Array.from({ length: 5 }, (_, i) => i); // [0,1,2,3,4] const e = Array.of(7); // [7], not empty x7 const f = Array.from("abc"); // ["a","b","c"] const g = Array.from(new Set(arr)); // dedupe 2D grid (watch the reference trap): const grid = Array.from({ length: rows }, () => new Array(cols).fill(0)); // WRONG: every row is the same array const bad = new Array(rows).fill(new Array(cols).fill(0)); ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- arr[0] arr.at(-1) // last element arr[arr.length - 1] arr.length // writable: arr.length = 0 empties it arr.length === 0 arr.push(x) // append, returns new length arr.pop() // remove and return last arr.unshift(x) // prepend, O(n) arr.shift() // remove and return first, O(n) arr.splice(i, count, ...items) // remove/insert in place arr.sort(cmp) // in place arr.reverse() // in place arr.fill(v, start, end) arr.copyWithin(target, start, end) arr.toSorted(cmp) // ES2023, copy arr.toReversed() arr.toSpliced(i, count, ...items) arr.with(i, value) // copy with one index changed // toSorted/toReversed/toSpliced/with // are the immutable copies; use in React arr.slice(start, end) // copy a range, negatives allowed arr.concat(other) arr.join(", ") arr.flat(depth) // flat(Infinity) for fully flat arr.flatMap(fn) [...arr] arr.includes(x) // uses SameValueZero, finds NaN arr.indexOf(x) // -1 if absent, does NOT find NaN arr.lastIndexOf(x) arr.find(fn) // first matching element or undefined arr.findIndex(fn) // -1 if none arr.findLast(fn) arr.findLastIndex(fn) arr.some(fn) // any match, short-circuits arr.every(fn) // all match, short-circuits ------------------------------------------------------------------------------- SORTING ------------------------------------------------------------------------------- Default sort is LEXICOGRAPHIC, even for numbers: [10, 9, 1].sort(); // [1, 10, 9] <-- wrong [10, 9, 1].sort((a, b) => a - b); // [1, 9, 10] Numbers ascending: (a, b) => a - b Numbers descending: (a, b) => b - a Strings: (a, b) => a.localeCompare(b) By property: (a, b) => a.age - b.age Multi-key: (a, b) => a.last.localeCompare(b.last) || a.first.localeCompare(b.first) Booleans first: (a, b) => Number(b.done) - Number(a.done) sort() is stable in all modern engines (required since ES2019). ------------------------------------------------------------------------------- COMMON LEETCODE PATTERNS ------------------------------------------------------------------------------- Dedupe: [...new Set(arr)] Sum: arr.reduce((a, b) => a + b, 0) Max: arr.reduce((a, b) => Math.max(a, b), -Infinity) Range: Array.from({ length: n }, (_, i) => i) Zip: a.map((x, i) => [x, b[i]]) Chunk: Array.from({ length: Math.ceil(a.length / n) }, (_, i) => a.slice(i * n, i * n + n)) Shuffle (Fisher-Yates): for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } Remove by value: arr.filter((x) => x !== target) Remove by index: arr.toSpliced(i, 1) Insert at index: arr.toSpliced(i, 0, item) Intersection: a.filter((x) => new Set(b).has(x)) Difference: a.filter((x) => !new Set(b).has(x)) Count truthy: arr.filter(Boolean).length Last n: arr.slice(-n) Rotate left by k: [...arr.slice(k), ...arr.slice(0, k)] ------------------------------------------------------------------------------- TIME COMPLEXITY ------------------------------------------------------------------------------- push / pop O(1) amortized shift / unshift O(n) indexOf / includes O(n) splice O(n) sort O(n log n) slice / concat / spread O(n) For a queue, do not use shift() in a loop on large arrays; that is O(n^2). Use an index pointer or a real deque. See QUEUE. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Calling sort() on numbers without a comparator. Forgetting sort(), reverse(), splice(), and fill() mutate in place. In React this means the state object is mutated and no re-render fires. Use toSorted / toReversed / toSpliced or copy first. Using new Array(n).fill([]) or .fill({}), which shares one reference across all slots. Using delete arr[i], which leaves a hole rather than shortening the array. Use splice() or filter(). Assuming array holes behave like undefined. map() and forEach() skip holes; for-of does not. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Knowing that shift() is O(n) and push/pop are O(1) is the difference between an O(n) and an O(n^2) BFS. Interviewers do notice. ================================================================================ ARRAY HIGHER ORDER METHODS ================================================================================ DESCRIPTION This is the JavaScript answer to LINQ. Unlike LINQ, these are eager: each call allocates a new array immediately. There is no deferred execution and no IEnumerable. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- map transform each element, same length out filter keep elements passing a test reduce fold the array into a single value const names = users.map((u) => u.name); const adults = users.filter((u) => u.age >= 18); const total = items.reduce((sum, i) => sum + i.price, 0); ------------------------------------------------------------------------------- FULL LIST ------------------------------------------------------------------------------- map(fn) transform filter(fn) select reduce(fn, init) fold left reduceRight(fn, init) fold right forEach(fn) side effects only, returns undefined find(fn) first match findIndex(fn) findLast(fn) findLastIndex(fn) some(fn) any every(fn) all flat(depth) flatten nested arrays flatMap(fn) map then flatten one level sort(cmp) order (mutates) toSorted(cmp) order (copy) entries() keys() values() iterators Object.groupBy(arr, fn) group into an object (ES2024) Map.groupBy(arr, fn) group into a Map (ES2024) ------------------------------------------------------------------------------- LINQ TO JAVASCRIPT TRANSLATION ------------------------------------------------------------------------------- Where -> filter Select -> map SelectMany -> flatMap OrderBy -> toSorted((a,b) => ...) OrderByDescending-> toSorted((a,b) => ...) reversed comparator ThenBy -> chained || in the comparator First -> arr[0], throws manually if needed FirstOrDefault -> arr.find(fn) Any -> some All -> every Count -> arr.filter(fn).length Sum -> reduce Min / Max -> Math.min(...arr) or reduce Distinct -> [...new Set(arr)] GroupBy -> Object.groupBy / reduce into a Map Skip / Take -> slice(n) / slice(0, n) ToDictionary -> Object.fromEntries / new Map Aggregate -> reduce ------------------------------------------------------------------------------- REDUCE PATTERNS ------------------------------------------------------------------------------- Sum: arr.reduce((a, b) => a + b, 0) Count by key: arr.reduce((acc, x) => { acc[x.type] = (acc[x.type] ?? 0) + 1; return acc; }, {}); Group by key (manual): arr.reduce((acc, x) => { (acc[x.type] ??= []).push(x); return acc; }, {}); Group by key (modern): Object.groupBy(arr, (x) => x.type); Map.groupBy(arr, (x) => x.type); Index by id: new Map(arr.map((x) => [x.id, x])); Object.fromEntries(arr.map((x) => [x.id, x])); Flatten: arr.reduce((a, b) => a.concat(b), []); // or just arr.flat() Max by property: arr.reduce((best, x) => (x.score > best.score ? x : best)); Always pass an initial value. reduce on an empty array without one throws TypeError. ------------------------------------------------------------------------------- CHAINING ------------------------------------------------------------------------------- const result = orders .filter((o) => o.status === "paid") .map((o) => ({ id: o.id, total: o.total })) .toSorted((a, b) => b.total - a.total) .slice(0, 10); Each step allocates a new array. For a few thousand items this is irrelevant; for millions, a single loop wins. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using map when nothing is returned. If you are not using the result, use forEach or for-of. Forgetting to return from the reduce callback, which makes the accumulator undefined on the next iteration. Using async callbacks with map and forgetting to await: const results = await Promise.all(arr.map(async (x) => load(x))); Expecting filter(async fn) to work. The callback returns a promise, which is always truthy, so nothing is filtered. Mutating the accumulator object AND returning a new one inconsistently inside reduce, producing subtle bugs. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Be ready to implement map, filter, and reduce from scratch on Array.prototype. It tests whether you understand the callback signature (element, index, array) and the this/thisArg parameter. ================================================================================ OBJECT AS HASHMAP ================================================================================ DESCRIPTION Plain objects are the quickest hash map for string keys. This is what most LeetCode JavaScript solutions use. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- const counts = {}; for (const c of str) { counts[c] = (counts[c] ?? 0) + 1; } "a" in counts Object.hasOwn(counts, "a") counts["a"] ?? 0 delete counts["a"]; Object.keys(counts).length ------------------------------------------------------------------------------- NULL PROTOTYPE DICTIONARY ------------------------------------------------------------------------------- const dict = Object.create(null); Avoids inherited keys like "constructor" and "toString" colliding with your data. Use this when keys come from user input. ------------------------------------------------------------------------------- OBJECT VS MAP ------------------------------------------------------------------------------- Use an object when: keys are known, fixed, string-like you want JSON serialization for free it is a record/DTO, not a collection Use a Map when: keys are dynamic or unknown at write time keys are objects, numbers, or mixed types you need reliable insertion order you add and delete frequently (Map is faster) you need .size ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- All object keys are strings (or symbols). obj[1] and obj["1"] are the same key. obj[someObject] becomes the key "[object Object]", silently collapsing every object key into one. Using "key" in obj when the prototype chain has that key. Use Object.hasOwn(). ================================================================================ MAP ================================================================================ DESCRIPTION Map is a keyed collection that accepts any value as a key, including objects. Insertion order is preserved. Prefer it over a plain object whenever keys are not fixed identifiers. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- const m = new Map(); const m = new Map([["a", 1], ["b", 2]]); const m = new Map(Object.entries(obj)); m.set(key, value) // returns the map, chainable m.get(key) // undefined if absent m.has(key) m.delete(key) // returns boolean m.clear() m.size // property, not a method m.keys() m.values() m.entries() m.forEach((value, key) => { }) for (const [k, v] of m) { } ------------------------------------------------------------------------------- CONVERSIONS ------------------------------------------------------------------------------- [...m] // [[k, v], ...] [...m.keys()] Object.fromEntries(m) // string keys only new Map(Object.entries(obj)) ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Counter: const counts = new Map(); for (const x of arr) counts.set(x, (counts.get(x) ?? 0) + 1); Group: const groups = new Map(); for (const x of arr) { if (!groups.has(x.type)) groups.set(x.type, []); groups.get(x.type).push(x); } Memo cache: const cache = new Map(); function fib(n) { if (n <= 1) return n; if (cache.has(n)) return cache.get(n); const r = fib(n - 1) + fib(n - 2); cache.set(n, r); return r; } Two-sum: const seen = new Map(); for (let i = 0; i < nums.length; i++) { if (seen.has(target - nums[i])) return [seen.get(target - nums[i]), i]; seen.set(nums[i], i); } Sort a Map by value: new Map([...m].sort((a, b) => b[1] - a[1])); ------------------------------------------------------------------------------- WEAKMAP ------------------------------------------------------------------------------- const wm = new WeakMap(); wm.set(objKey, metadata); Keys must be objects and are held weakly, so entries disappear when the key is garbage collected. Not iterable, no .size. Use for attaching private data or caches keyed by object identity without leaking. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Writing m.size() instead of m.size. Using object literals as keys and expecting value equality. Map keys compare by reference, so { a: 1 } will never find { a: 1 }. Using JSON.stringify(map), which produces {}. Convert with Object.fromEntries or [...map] first. ================================================================================ SET ================================================================================ DESCRIPTION Set stores unique values of any type, using SameValueZero equality. The usual tool for deduping, membership tests, and set algebra. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- const s = new Set(); const s = new Set([1, 2, 2, 3]); // {1, 2, 3} const s = new Set("hello"); // unique characters s.add(x) // chainable s.has(x) // O(1) s.delete(x) s.clear() s.size for (const x of s) { } [...s] Array.from(s) ------------------------------------------------------------------------------- SET OPERATIONS (ES2025) ------------------------------------------------------------------------------- a.union(b) a.intersection(b) a.difference(b) a.symmetricDifference(b) a.isSubsetOf(b) a.isSupersetOf(b) a.isDisjointFrom(b) Manual versions for older runtimes: const union = new Set([...a, ...b]); const inter = new Set([...a].filter((x) => b.has(x))); const diff = new Set([...a].filter((x) => !b.has(x))); ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Dedupe: [...new Set(arr)] Has duplicates: new Set(arr).size !== arr.length Seen tracking: const seen = new Set(); if (seen.has(x)) ... Visited in BFS/DFS: visited.add(`${r},${c}`) ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Adding objects and expecting deduplication by value. Sets use SameValueZero, which is reference equality for objects. NaN behaves specially and correctly: a Set holds at most one NaN even though NaN !== NaN. Using a Set to dedupe coordinate pairs by pushing arrays. [1,2] and [1,2] are different references. Use a string key or a nested Map. ================================================================================ QUEUE ================================================================================ DESCRIPTION JavaScript has no built-in queue. There are three practical options. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- const q = []; q.push(x); const front = q.shift(); // O(n), fine under a few thousand items ------------------------------------------------------------------------------- ARRAY WITH HEAD POINTER (PREFERRED FOR BFS) ------------------------------------------------------------------------------- const q = [start]; let head = 0; while (head < q.length) { const node = q[head++]; for (const next of neighbors(node)) q.push(next); } O(1) per operation, trades memory for speed. This is the standard LeetCode BFS pattern in JavaScript. ------------------------------------------------------------------------------- LINKED LIST QUEUE (CONSTANT MEMORY) ------------------------------------------------------------------------------- class Queue { #head = null; #tail = null; #size = 0; enqueue(value) { const node = { value, next: null }; if (this.#tail) this.#tail.next = node; else this.#head = node; this.#tail = node; this.#size++; } dequeue() { if (!this.#head) return undefined; const { value } = this.#head; this.#head = this.#head.next; if (!this.#head) this.#tail = null; this.#size--; return value; } peek() { return this.#head?.value; } get size() { return this.#size; } get isEmpty() { return this.#size === 0; } } ================================================================================ STACK ================================================================================ DESCRIPTION A stack is last-in first-out. In JavaScript a plain array with push and pop is the stack; both ends of that pair are amortized O(1). ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- const stack = []; stack.push(x); const top = stack.pop(); const peek = stack.at(-1); stack.length === 0; ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Balanced parentheses: function isValid(s) { const pairs = { ")": "(", "]": "[", "}": "{" }; const stack = []; for (const c of s) { if (c in pairs) { if (stack.pop() !== pairs[c]) return false; } else { stack.push(c); } } return stack.length === 0; } Monotonic stack (next greater element): function nextGreater(nums) { const res = new Array(nums.length).fill(-1); const stack = []; // holds indices for (let i = 0; i < nums.length; i++) { while (stack.length && nums[stack.at(-1)] < nums[i]) { res[stack.pop()] = nums[i]; } stack.push(i); } return res; } ================================================================================ LINKED LIST ================================================================================ DESCRIPTION Singly-linked nodes with val and next. LeetCode provides the node constructor. Dummy heads, reversal, and fast/slow pointers are the patterns that come up constantly. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- function ListNode(val, next) { this.val = val === undefined ? 0 : val; this.next = next === undefined ? null : next; } Or as a plain object: { val, next } ------------------------------------------------------------------------------- TRAVERSE ------------------------------------------------------------------------------- let curr = head; while (curr) { console.log(curr.val); curr = curr.next; } ------------------------------------------------------------------------------- REVERSE ------------------------------------------------------------------------------- function reverseList(head) { let prev = null; let curr = head; while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } ------------------------------------------------------------------------------- FAST AND SLOW POINTERS ------------------------------------------------------------------------------- Middle of the list: let slow = head, fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; } return slow; Cycle detection (Floyd): function hasCycle(head) { let slow = head, fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false; } Nth from end: let lead = head; for (let i = 0; i < n; i++) lead = lead.next; let trail = head; while (lead) { lead = lead.next; trail = trail.next; } ------------------------------------------------------------------------------- DUMMY HEAD PATTERN ------------------------------------------------------------------------------- Removes the special case for modifying the head: function removeElements(head, val) { const dummy = new ListNode(0, head); let curr = dummy; while (curr.next) { if (curr.next.val === val) curr.next = curr.next.next; else curr = curr.next; } return dummy.next; } ------------------------------------------------------------------------------- MERGE TWO SORTED LISTS ------------------------------------------------------------------------------- function mergeTwoLists(a, b) { const dummy = new ListNode(); let tail = dummy; while (a && b) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a ?? b; return dummy.next; } ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Nearly every linked list problem is one of: reverse, fast/slow pointers, dummy head, or merge. Recognizing which one is most of the work. ================================================================================ HEAP ================================================================================ DESCRIPTION JavaScript has no PriorityQueue. You must implement a binary heap, and it is worth memorizing this one. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- class MinHeap { constructor(compare = (a, b) => a - b) { this.heap = []; this.compare = compare; } get size() { return this.heap.length; } peek() { return this.heap[0]; } push(value) { this.heap.push(value); this.#bubbleUp(this.heap.length - 1); } pop() { if (this.heap.length === 0) return undefined; const top = this.heap[0]; const last = this.heap.pop(); if (this.heap.length > 0) { this.heap[0] = last; this.#bubbleDown(0); } return top; } #bubbleUp(i) { while (i > 0) { const parent = (i - 1) >> 1; if (this.compare(this.heap[i], this.heap[parent]) >= 0) break; [this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]]; i = parent; } } #bubbleDown(i) { const n = this.heap.length; while (true) { const left = 2 * i + 1; const right = 2 * i + 2; let smallest = i; if (left < n && this.compare(this.heap[left], this.heap[smallest]) < 0) { smallest = left; } if (right < n && this.compare(this.heap[right], this.heap[smallest]) < 0) { smallest = right; } if (smallest === i) break; [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]]; i = smallest; } } } ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Min heap of numbers: const h = new MinHeap(); Max heap: const h = new MinHeap((a, b) => b - a); Heap of objects by priority: const h = new MinHeap((a, b) => a.dist - b.dist); h.push({ node: 3, dist: 7 }); ------------------------------------------------------------------------------- K LARGEST ELEMENTS ------------------------------------------------------------------------------- function kLargest(nums, k) { const h = new MinHeap(); for (const n of nums) { h.push(n); if (h.size > k) h.pop(); } return h.heap; } O(n log k), better than sorting when k is small. ------------------------------------------------------------------------------- TIME COMPLEXITY ------------------------------------------------------------------------------- push O(log n) pop O(log n) peek O(1) build O(n) via heapify, O(n log n) via repeated push ================================================================================ BINARY SEARCH ================================================================================ DESCRIPTION A divide-and-conquer search over a sorted range that halves the remaining candidates each step. The same skeleton searches the answer space when the predicate is monotonic. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- function binarySearch(nums, target) { let lo = 0, hi = nums.length - 1; while (lo <= hi) { const mid = lo + ((hi - lo) >> 1); if (nums[mid] === target) return mid; if (nums[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ------------------------------------------------------------------------------- LEFTMOST INSERTION POINT (LOWER BOUND) ------------------------------------------------------------------------------- function lowerBound(nums, target) { let lo = 0, hi = nums.length; while (lo < hi) { const mid = (lo + hi) >> 1; if (nums[mid] < target) lo = mid + 1; else hi = mid; } return lo; } ------------------------------------------------------------------------------- RIGHTMOST INSERTION POINT (UPPER BOUND) ------------------------------------------------------------------------------- function upperBound(nums, target) { let lo = 0, hi = nums.length; while (lo < hi) { const mid = (lo + hi) >> 1; if (nums[mid] <= target) lo = mid + 1; else hi = mid; } return lo; } ------------------------------------------------------------------------------- BINARY SEARCH ON ANSWER ------------------------------------------------------------------------------- When the answer space is monotonic (feasible below a threshold, infeasible above), binary search the answer itself: function minCapacity(weights, days) { const canShip = (cap) => { let need = 1, load = 0; for (const w of weights) { if (load + w > cap) { need++; load = 0; } load += w; } return need <= days; }; let lo = Math.max(...weights); let hi = weights.reduce((a, b) => a + b, 0); while (lo < hi) { const mid = (lo + hi) >> 1; if (canShip(mid)) hi = mid; else lo = mid + 1; } return lo; } ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using (lo + hi) / 2 without flooring. In JavaScript this produces a float index. Use >> 1 or Math.floor. Mixing the <= hi and < hi loop forms with the wrong hi initialization. Pick one template and stick with it. Infinite loops when neither bound moves. Any branch that sets lo = mid (rather than mid + 1) needs the mid calculation to round up. ================================================================================ TREE DFS ================================================================================ DESCRIPTION Depth-first walks of a binary tree: recursive preorder/inorder/postorder and the iterative equivalents. Most tree interview problems are a DFS with a little extra state. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- function TreeNode(val, left, right) { this.val = val === undefined ? 0 : val; this.left = left === undefined ? null : left; this.right = right === undefined ? null : right; } ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- function inorder(node, out = []) { if (!node) return out; inorder(node.left, out); out.push(node.val); inorder(node.right, out); return out; } Preorder: visit, left, right Inorder: left, visit, right (sorted order for a BST) Postorder: left, right, visit ------------------------------------------------------------------------------- ITERATIVE INORDER ------------------------------------------------------------------------------- function inorderIterative(root) { const out = [], stack = []; let curr = root; while (curr || stack.length) { while (curr) { stack.push(curr); curr = curr.left; } curr = stack.pop(); out.push(curr.val); curr = curr.right; } return out; } ------------------------------------------------------------------------------- ITERATIVE PREORDER ------------------------------------------------------------------------------- function preorderIterative(root) { if (!root) return []; const out = [], stack = [root]; while (stack.length) { const node = stack.pop(); out.push(node.val); if (node.right) stack.push(node.right); if (node.left) stack.push(node.left); } return out; } ------------------------------------------------------------------------------- COMMON PROBLEMS ------------------------------------------------------------------------------- Max depth: const maxDepth = (n) => n ? 1 + Math.max(maxDepth(n.left), maxDepth(n.right)) : 0; Invert: function invertTree(root) { if (!root) return null; [root.left, root.right] = [invertTree(root.right), invertTree(root.left)]; return root; } Same tree: const isSame = (a, b) => (!a && !b) || (!!a && !!b && a.val === b.val && isSame(a.left, b.left) && isSame(a.right, b.right)); Validate BST: function isValidBST(node, lo = -Infinity, hi = Infinity) { if (!node) return true; if (node.val <= lo || node.val >= hi) return false; return isValidBST(node.left, lo, node.val) && isValidBST(node.right, node.val, hi); } Path sum: function hasPathSum(node, target) { if (!node) return false; if (!node.left && !node.right) return node.val === target; const rest = target - node.val; return hasPathSum(node.left, rest) || hasPathSum(node.right, rest); } Lowest common ancestor: function lca(root, p, q) { if (!root || root === p || root === q) return root; const l = lca(root.left, p, q); const r = lca(root.right, p, q); return l && r ? root : (l ?? r); } ================================================================================ TREE BFS ================================================================================ DESCRIPTION Level-order traversal using a queue. Avoid Array.shift in a loop on large inputs; use a head pointer or swap arrays so BFS stays O(n). ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- function levelOrder(root) { if (!root) return []; const out = []; let queue = [root]; while (queue.length) { const level = []; const next = []; for (const node of queue) { level.push(node.val); if (node.left) next.push(node.left); if (node.right) next.push(node.right); } out.push(level); queue = next; } return out; } The swap-arrays form avoids shift() entirely and keeps levels separate without tracking sizes. ------------------------------------------------------------------------------- WITH A HEAD POINTER ------------------------------------------------------------------------------- const q = [root]; let head = 0; while (head < q.length) { const size = q.length - head; for (let i = 0; i < size; i++) { const node = q[head++]; if (node.left) q.push(node.left); if (node.right) q.push(node.right); } } ------------------------------------------------------------------------------- VARIANTS ------------------------------------------------------------------------------- Right side view: last element of each level Zigzag: reverse every other level Min depth: first level where a node has no children Level averages: level.reduce((a,b)=>a+b,0) / level.length ================================================================================ BACKTRACKING ================================================================================ DESCRIPTION Build a candidate incrementally, recurse, then undo. The undo step is the whole point. Always push a copy of the path into results, never the live array. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- function backtrack(path, choices, results) { if (isComplete(path)) { results.push([...path]); // copy, always return; } for (const choice of choices) { if (!isValid(path, choice)) continue; path.push(choice); backtrack(path, nextChoices(choices, choice), results); path.pop(); // undo } } ------------------------------------------------------------------------------- SUBSETS ------------------------------------------------------------------------------- function subsets(nums) { const res = [], path = []; const dfs = (start) => { res.push([...path]); for (let i = start; i < nums.length; i++) { path.push(nums[i]); dfs(i + 1); path.pop(); } }; dfs(0); return res; } ------------------------------------------------------------------------------- PERMUTATIONS ------------------------------------------------------------------------------- function permute(nums) { const res = [], path = [], used = new Array(nums.length).fill(false); const dfs = () => { if (path.length === nums.length) { res.push([...path]); return; } for (let i = 0; i < nums.length; i++) { if (used[i]) continue; used[i] = true; path.push(nums[i]); dfs(); path.pop(); used[i] = false; } }; dfs(); return res; } ------------------------------------------------------------------------------- COMBINATION SUM ------------------------------------------------------------------------------- function combinationSum(candidates, target) { const res = [], path = []; const dfs = (start, remain) => { if (remain === 0) { res.push([...path]); return; } if (remain < 0) return; for (let i = start; i < candidates.length; i++) { path.push(candidates[i]); dfs(i, remain - candidates[i]); // i, reuse allowed path.pop(); } }; dfs(0, target); return res; } ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Pushing path directly instead of [...path]. Arrays are references, so every result ends up pointing at the same (eventually empty) array. Forgetting to undo state after the recursive call. Skipping duplicates incorrectly. The standard trick is to sort first, then skip when i > start && nums[i] === nums[i - 1]. ================================================================================ SLIDING WINDOW ================================================================================ DESCRIPTION A contiguous subarray or substring whose left and right edges only move forward. Use it for 'longest/shortest subarray that satisfies X' in O(n). ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- function maxSum(nums, k) { let sum = 0; for (let i = 0; i < k; i++) sum += nums[i]; let best = sum; for (let i = k; i < nums.length; i++) { sum += nums[i] - nums[i - k]; best = Math.max(best, sum); } return best; } ------------------------------------------------------------------------------- VARIABLE SIZE ------------------------------------------------------------------------------- function longestUniqueSubstring(s) { const seen = new Set(); let left = 0, best = 0; for (let right = 0; right < s.length; right++) { while (seen.has(s[right])) { seen.delete(s[left]); left++; } seen.add(s[right]); best = Math.max(best, right - left + 1); } return best; } ------------------------------------------------------------------------------- WITH A COUNT MAP ------------------------------------------------------------------------------- function minWindow(s, t) { const need = new Map(); for (const c of t) need.set(c, (need.get(c) ?? 0) + 1); let missing = t.length, left = 0, best = [0, Infinity]; for (let right = 0; right < s.length; right++) { if ((need.get(s[right]) ?? 0) > 0) missing--; need.set(s[right], (need.get(s[right]) ?? 0) - 1); while (missing === 0) { if (right - left < best[1] - best[0]) best = [left, right]; need.set(s[left], (need.get(s[left]) ?? 0) + 1); if (need.get(s[left]) > 0) missing++; left++; } } return best[1] === Infinity ? "" : s.slice(best[0], best[1] + 1); } ------------------------------------------------------------------------------- WHEN TO REACH FOR IT ------------------------------------------------------------------------------- Contiguous subarray or substring, plus "longest", "shortest", "max sum", "at most k distinct", or "contains all of". If the problem is about a contiguous range and the brute force is O(n^2), a window is usually the O(n) answer. ================================================================================ TWO POINTERS ================================================================================ DESCRIPTION Two indices walking an array from opposite ends or in the same direction. Sorted input, in-place compaction, palindromes, and k-sum all use this. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- function twoSumSorted(nums, target) { let lo = 0, hi = nums.length - 1; while (lo < hi) { const sum = nums[lo] + nums[hi]; if (sum === target) return [lo, hi]; if (sum < target) lo++; else hi--; } return []; } ------------------------------------------------------------------------------- SAME DIRECTION (READ / WRITE) ------------------------------------------------------------------------------- function removeDuplicates(nums) { let write = 1; for (let read = 1; read < nums.length; read++) { if (nums[read] !== nums[read - 1]) nums[write++] = nums[read]; } return write; } ------------------------------------------------------------------------------- PALINDROME CHECK ------------------------------------------------------------------------------- function isPalindrome(s) { const t = s.toLowerCase().replace(/[^a-z0-9]/g, ""); let lo = 0, hi = t.length - 1; while (lo < hi) { if (t[lo++] !== t[hi--]) return false; } return true; } ------------------------------------------------------------------------------- THREE SUM ------------------------------------------------------------------------------- function threeSum(nums) { nums.sort((a, b) => a - b); const res = []; for (let i = 0; i < nums.length - 2; i++) { if (i > 0 && nums[i] === nums[i - 1]) continue; let lo = i + 1, hi = nums.length - 1; while (lo < hi) { const sum = nums[i] + nums[lo] + nums[hi]; if (sum < 0) lo++; else if (sum > 0) hi--; else { res.push([nums[i], nums[lo], nums[hi]]); while (lo < hi && nums[lo] === nums[lo + 1]) lo++; while (lo < hi && nums[hi] === nums[hi - 1]) hi--; lo++; hi--; } } } return res; } ================================================================================ UNION FIND ================================================================================ DESCRIPTION Disjoint-set with path compression and union by rank. The tool for connectivity, cycle detection in undirected graphs, and 'number of islands' variants. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- class UnionFind { constructor(n) { this.parent = Array.from({ length: n }, (_, i) => i); this.rank = new Array(n).fill(0); this.count = n; } find(x) { while (this.parent[x] !== x) { this.parent[x] = this.parent[this.parent[x]]; // compress x = this.parent[x]; } return x; } union(a, b) { const ra = this.find(a), rb = this.find(b); if (ra === rb) return false; if (this.rank[ra] < this.rank[rb]) this.parent[ra] = rb; else if (this.rank[ra] > this.rank[rb]) this.parent[rb] = ra; else { this.parent[rb] = ra; this.rank[ra]++; } this.count--; return true; } connected(a, b) { return this.find(a) === this.find(b); } } ------------------------------------------------------------------------------- WHEN TO USE ------------------------------------------------------------------------------- Counting connected components, detecting cycles in an undirected graph, Kruskal's MST, "number of provinces", "redundant connection", and accounts/email merging problems. With path compression and union by rank, operations are effectively O(1) (inverse Ackermann). ================================================================================ TRIE ================================================================================ DESCRIPTION A prefix tree. Each node holds a map of children and an end-of-word flag. Insert and search are O(length of the word), independent of how many words are stored. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- class TrieNode { constructor() { this.children = new Map(); this.isEnd = false; } } class Trie { constructor() { this.root = new TrieNode(); } insert(word) { let node = this.root; for (const c of word) { if (!node.children.has(c)) node.children.set(c, new TrieNode()); node = node.children.get(c); } node.isEnd = true; } #walk(prefix) { let node = this.root; for (const c of prefix) { node = node.children.get(c); if (!node) return null; } return node; } search(word) { return this.#walk(word)?.isEnd ?? false; } startsWith(prefix) { return this.#walk(prefix) !== null; } } Use for autocomplete, prefix matching, word search on a board, and "longest common prefix" problems. Insert and search are O(length of word), independent of how many words are stored. ================================================================================ RECURSION AND MEMOIZATION ================================================================================ DESCRIPTION Every recursion needs a base case and progress toward it. Memoization caches results by arguments so overlapping subproblems are not recomputed. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Every recursion needs a base case and progress toward it. const factorial = (n) => (n <= 1 ? 1 : n * factorial(n - 1)); There is no tail call optimization in V8 (Safari is the exception), so deep recursion hits "Maximum call stack size exceeded" around 10k frames. Convert to iteration for deep inputs. ------------------------------------------------------------------------------- MEMOIZATION ------------------------------------------------------------------------------- Generic: 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; }; } Hand-rolled for a single function: function fib(n, memo = new Map()) { if (n <= 1) return n; if (memo.has(n)) return memo.get(n); const r = fib(n - 1, memo) + fib(n - 2, memo); memo.set(n, r); return r; } Grid memo: const memo = Array.from({ length: m }, () => new Array(n).fill(-1)); ------------------------------------------------------------------------------- CONVERTING RECURSION TO ITERATION ------------------------------------------------------------------------------- Replace the call stack with an explicit stack: function dfsIterative(root) { const stack = [root]; while (stack.length) { const node = stack.pop(); if (!node) continue; visit(node); stack.push(node.right, node.left); } } ================================================================================ DYNAMIC PROGRAMMING ================================================================================ DESCRIPTION Solve overlapping subproblems by storing answers, top-down (memoized recursion) or bottom-up (a table). The skill is recognizing the state, not the syntax. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Top-down: recursion + memo. Easier to derive from brute force. Bottom-up: fill a table iteratively. Faster, no stack limit. ------------------------------------------------------------------------------- 1D DP ------------------------------------------------------------------------------- Climbing stairs: function climbStairs(n) { let a = 1, b = 1; for (let i = 2; i <= n; i++) [a, b] = [b, a + b]; return b; } House robber: function rob(nums) { let prev = 0, curr = 0; for (const n of nums) [prev, curr] = [curr, Math.max(curr, prev + n)]; return curr; } Coin change: function coinChange(coins, amount) { const dp = new Array(amount + 1).fill(Infinity); dp[0] = 0; for (let i = 1; i <= amount; i++) { for (const c of coins) { if (c <= i) dp[i] = Math.min(dp[i], dp[i - c] + 1); } } return dp[amount] === Infinity ? -1 : dp[amount]; } ------------------------------------------------------------------------------- 2D DP ------------------------------------------------------------------------------- Longest common subsequence: function lcs(a, b) { const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0)); for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]); } } return dp[a.length][b.length]; } Unique paths: function uniquePaths(m, n) { const dp = new Array(n).fill(1); for (let i = 1; i < m; i++) { for (let j = 1; j < n; j++) dp[j] += dp[j - 1]; } return dp[n - 1]; } ------------------------------------------------------------------------------- RECOGNIZING DP ------------------------------------------------------------------------------- Signals: "how many ways", "minimum/maximum cost", "can you reach", overlapping subproblems, and a decision at each step whose optimal answer depends on optimal answers to smaller inputs. Define the state first, in one sentence: "dp[i] is the answer for the first i elements". Then find the transition. Then find the base case. ================================================================================ BIT MANIPULATION ================================================================================ DESCRIPTION Bitwise operators coerce to 32-bit signed integers. For values above 2^31 - 1 use BigInt or ordinary arithmetic. XOR, masks, and n & (n - 1) are the interview staples. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- Bitwise operators coerce to 32-bit signed integers. For values above 2^31 - 1 use BigInt or arithmetic instead. Check bit i: (n >> i) & 1 Set bit i: n | (1 << i) Clear bit i: n & ~(1 << i) Toggle bit i: n ^ (1 << i) Is even: (n & 1) === 0 Multiply by 2: n << 1 Divide by 2: n >> 1 Clear lowest set bit: n & (n - 1) Isolate lowest set bit: n & -n Is power of two: n > 0 && (n & (n - 1)) === 0 Count set bits: let c = 0; while (n) { n &= n - 1; c++; } Swap without temp: a ^= b; b ^= a; a ^= b; All ones mask of k: (1 << k) - 1 XOR of duplicates: arr.reduce((a, b) => a ^ b, 0) Subset enumeration with a bitmask: for (let mask = 0; mask < (1 << n); mask++) { const subset = []; for (let i = 0; i < n; i++) { if (mask & (1 << i)) subset.push(nums[i]); } } Single number (every other element appears twice): const singleNumber = (nums) => nums.reduce((a, b) => a ^ b, 0); ================================================================================ GRAPHS ================================================================================ DESCRIPTION Nodes and edges. Default representation is an adjacency list (Map of arrays). DFS, BFS, topological sort, Dijkstra, and cycle detection cover almost every interview graph. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- Adjacency list (default choice): const graph = new Map(); function addEdge(a, b) { if (!graph.has(a)) graph.set(a, []); if (!graph.has(b)) graph.set(b, []); graph.get(a).push(b); graph.get(b).push(a); // omit for directed } From an edge list: const graph = Array.from({ length: n }, () => []); for (const [a, b] of edges) { graph[a].push(b); graph[b].push(a); } Grid as an implicit graph: const dirs = [[0,1],[1,0],[0,-1],[-1,0]]; ------------------------------------------------------------------------------- DFS ------------------------------------------------------------------------------- function dfs(graph, start, visited = new Set()) { if (visited.has(start)) return; visited.add(start); for (const next of graph.get(start) ?? []) dfs(graph, next, visited); } Iterative: const stack = [start], visited = new Set(); while (stack.length) { const node = stack.pop(); if (visited.has(node)) continue; visited.add(node); for (const next of graph.get(node) ?? []) stack.push(next); } ------------------------------------------------------------------------------- BFS AND SHORTEST PATH (UNWEIGHTED) ------------------------------------------------------------------------------- function bfs(graph, start, target) { const q = [[start, 0]]; let head = 0; const visited = new Set([start]); while (head < q.length) { const [node, dist] = q[head++]; if (node === target) return dist; for (const next of graph.get(node) ?? []) { if (!visited.has(next)) { visited.add(next); q.push([next, dist + 1]); } } } return -1; } ------------------------------------------------------------------------------- GRID FLOOD FILL ------------------------------------------------------------------------------- function numIslands(grid) { const rows = grid.length, cols = grid[0].length; let count = 0; const fill = (r, c) => { if (r < 0 || c < 0 || r >= rows || c >= cols) return; if (grid[r][c] !== "1") return; grid[r][c] = "0"; fill(r + 1, c); fill(r - 1, c); fill(r, c + 1); fill(r, c - 1); }; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === "1") { count++; fill(r, c); } } } return count; } ------------------------------------------------------------------------------- TOPOLOGICAL SORT (KAHN) ------------------------------------------------------------------------------- function topoSort(n, edges) { const graph = Array.from({ length: n }, () => []); const indeg = new Array(n).fill(0); for (const [from, to] of edges) { graph[from].push(to); indeg[to]++; } const q = []; for (let i = 0; i < n; i++) if (indeg[i] === 0) q.push(i); const order = []; let head = 0; while (head < q.length) { const node = q[head++]; order.push(node); for (const next of graph[node]) { if (--indeg[next] === 0) q.push(next); } } return order.length === n ? order : []; // [] means a cycle } ------------------------------------------------------------------------------- DIJKSTRA ------------------------------------------------------------------------------- function dijkstra(graph, start, n) { const dist = new Array(n).fill(Infinity); dist[start] = 0; const pq = new MinHeap((a, b) => a[0] - b[0]); // [dist, node] pq.push([0, start]); while (pq.size) { const [d, node] = pq.pop(); if (d > dist[node]) continue; for (const [next, weight] of graph[node]) { const nd = d + weight; if (nd < dist[next]) { dist[next] = nd; pq.push([nd, next]); } } } return dist; } ------------------------------------------------------------------------------- CYCLE DETECTION IN A DIRECTED GRAPH ------------------------------------------------------------------------------- Three-color DFS: 0 unvisited, 1 in progress, 2 done. function hasCycle(graph, n) { const state = new Array(n).fill(0); const dfs = (node) => { if (state[node] === 1) return true; if (state[node] === 2) return false; state[node] = 1; for (const next of graph[node]) if (dfs(next)) return true; state[node] = 2; return false; }; for (let i = 0; i < n; i++) if (dfs(i)) return true; return false; } ------------------------------------------------------------------------------- CHOOSING AN ALGORITHM ------------------------------------------------------------------------------- Unweighted shortest path BFS Weighted, non-negative Dijkstra Weighted, negative edges Bellman-Ford All pairs Floyd-Warshall Connectivity / components DFS or Union Find Ordering with dependencies Topological sort Minimum spanning tree Kruskal (Union Find) or Prim (heap) ================================================================================ ITERATORS AND GENERATORS ================================================================================ DESCRIPTION An iterable is any object with [Symbol.iterator](). Generators (function*) produce iterators lazily and are the clean way to write custom iteration. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- An object is iterable if it has a [Symbol.iterator] method returning an iterator: an object with next() returning { value, done }. Built-in iterables: Array, String, Map, Set, TypedArray, arguments, NodeList, and generators. Custom iterable: const range = { from: 1, to: 5, [Symbol.iterator]() { let curr = this.from, last = this.to; return { next: () => curr <= last ? { value: curr++, done: false } : { value: undefined, done: true } }; } }; [...range]; // [1, 2, 3, 4, 5] ------------------------------------------------------------------------------- GENERATORS ------------------------------------------------------------------------------- function* range(from, to, step = 1) { for (let i = from; i <= to; i += step) yield i; } [...range(1, 5)]; // [1, 2, 3, 4, 5] const gen = range(1, 3); gen.next(); // { value: 1, done: false } gen.next(); // { value: 2, done: false } Delegation: function* combined() { yield* range(1, 3); yield* [10, 20]; } Infinite sequences are safe because generators are lazy: function* naturals() { let n = 0; while (true) yield n++; } function* take(it, n) { let i = 0; for (const x of it) { if (i++ >= n) return; yield x; } } This is the closest JavaScript comes to IEnumerable/yield return in C#, and it behaves almost identically. ------------------------------------------------------------------------------- TWO-WAY COMMUNICATION ------------------------------------------------------------------------------- function* dialogue() { const name = yield "What is your name?"; yield `Hello, ${name}`; } const d = dialogue(); d.next().value; // "What is your name?" d.next("Jake").value; // "Hello, Jake" ------------------------------------------------------------------------------- ASYNC GENERATORS ------------------------------------------------------------------------------- async function* paginate(url) { let next = url; while (next) { const res = await fetch(next); const page = await res.json(); yield* page.items; next = page.nextUrl; } } for await (const item of paginate("/api/items")) { console.log(item); } ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Generators are worth knowing for lazy pagination, infinite sequences, and as the mechanism Redux Saga is built on. They are also the honest answer to "how would you process a stream too large for memory". ================================================================================ ASYNC ================================================================================ DESCRIPTION Promises, async/await, parallelism vs sequencing, cancellation, retries, and wrapping callback APIs. Almost all modern JavaScript I/O is promise-based. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- A promise is in one of three states: pending, fulfilled, or rejected. Once settled it never changes. const p = new Promise((resolve, reject) => { setTimeout(() => resolve("done"), 1000); }); p.then((value) => { }) .catch((err) => { }) .finally(() => { }); Static helpers: Promise.resolve(value) Promise.reject(error) Promise.all([p1, p2]) // all succeed, or reject on first fail Promise.allSettled([p1, p2]) // never rejects, array of results Promise.race([p1, p2]) // first to settle, win or lose Promise.any([p1, p2]) // first to FULFILL, AggregateError // if all reject ------------------------------------------------------------------------------- ASYNC AWAIT ------------------------------------------------------------------------------- async function loadUser(id) { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } An async function always returns a promise. await unwraps one. Top-level await works in ESM modules without wrapping. ------------------------------------------------------------------------------- SEQUENTIAL VS PARALLEL ------------------------------------------------------------------------------- Sequential (slow, when independent): const a = await loadA(); const b = await loadB(); Parallel (fast): const [a, b] = await Promise.all([loadA(), loadB()]); Start now, await later: const pa = loadA(); const pb = loadB(); const a = await pa; const b = await pb; Partial failure tolerated: const results = await Promise.allSettled([loadA(), loadB()]); for (const r of results) { if (r.status === "fulfilled") use(r.value); else log(r.reason); } ------------------------------------------------------------------------------- LOOPING WITH AWAIT ------------------------------------------------------------------------------- Sequential (order matters, or to avoid rate limits): for (const id of ids) { await process(id); } Parallel: await Promise.all(ids.map((id) => process(id))); Bounded concurrency: async function pool(items, limit, fn) { const results = []; const running = new Set(); for (const item of items) { const p = Promise.resolve().then(() => fn(item)); results.push(p); running.add(p); p.finally(() => running.delete(p)); if (running.size >= limit) await Promise.race(running); } return Promise.all(results); } ------------------------------------------------------------------------------- TIMEOUTS AND CANCELLATION ------------------------------------------------------------------------------- const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); Timeout wrapper: function withTimeout(promise, ms) { return Promise.race([ promise, sleep(ms).then(() => { throw new Error("timeout"); }) ]); } AbortController (the real cancellation mechanism): const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); try { const res = await fetch(url, { signal: controller.signal }); } catch (err) { if (err.name === "AbortError") console.log("cancelled"); } Shortcut: fetch(url, { signal: AbortSignal.timeout(5000) }); AbortController is the JavaScript equivalent of CancellationToken. ------------------------------------------------------------------------------- RETRY WITH BACKOFF ------------------------------------------------------------------------------- async function retry(fn, attempts = 3, delay = 300) { for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (err) { if (i === attempts - 1) throw err; await sleep(delay * 2 ** i); } } } ------------------------------------------------------------------------------- PROMISIFYING A CALLBACK API ------------------------------------------------------------------------------- import { promisify } from "node:util"; const readFile = promisify(fs.readFile); Manual: const read = (path) => new Promise((resolve, reject) => { fs.readFile(path, "utf8", (err, data) => err ? reject(err) : resolve(data)); }); ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Forgetting await, which returns a pending promise instead of a value. Symptoms: "[object Promise]" in output, or undefined properties. Awaiting in a loop when the calls are independent, turning parallel work into sequential work. Using await inside forEach. The callback is async but forEach ignores the returned promise. Creating a promise around an already-async function (the explicit construction antipattern): // wrong return new Promise((resolve) => resolve(fetchData())); // right return fetchData(); Not handling rejections, producing an unhandled rejection warning which in Node crashes the process by default. Assuming fetch rejects on 404 or 500. It does not; check res.ok. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- Be ready to implement Promise.all from scratch, and to explain the difference between all, allSettled, race, and any. Also be ready to explain that async/await is syntax over promises, not over threads: there is exactly one JavaScript thread. ================================================================================ EVENT LOOP ================================================================================ DESCRIPTION JavaScript is single-threaded. Concurrency comes from the event loop handing async work to the host (browser or libuv) and running the callbacks later. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Call stack synchronous code runs here, one frame at a time Web APIs / libuv timers, I/O, network run outside the JS thread Microtask queue promise callbacks, queueMicrotask, MutationObserver Macrotask queue setTimeout, setInterval, I/O callbacks, UI events Order of operations per tick: 1. run all synchronous code to completion 2. drain the ENTIRE microtask queue 3. run ONE macrotask 4. repeat from step 2 Microtasks always beat macrotasks, and the microtask queue is fully drained before a single timer callback runs. ------------------------------------------------------------------------------- CLASSIC ORDERING PUZZLE ------------------------------------------------------------------------------- console.log("1"); setTimeout(() => console.log("2"), 0); Promise.resolve().then(() => console.log("3")); queueMicrotask(() => console.log("4")); console.log("5"); Output: 1, 5, 3, 4, 2 Sync first (1, 5), then microtasks in order (3, 4), then the timer (2). ------------------------------------------------------------------------------- STARVATION ------------------------------------------------------------------------------- A microtask that queues another microtask forever blocks all timers and all rendering permanently: function loop() { Promise.resolve().then(loop); } // freezes the tab A long synchronous loop blocks everything too. For CPU-heavy work in the browser, use a Web Worker; in Node, worker_threads or a child process. ------------------------------------------------------------------------------- NODE SPECIFICS ------------------------------------------------------------------------------- process.nextTick() runs before other microtasks, Node only setImmediate() runs in the check phase, after I/O callbacks Node's loop phases: timers -> pending callbacks -> poll -> check (setImmediate) -> close callbacks, with the microtask queue drained between each. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- "Explain the event loop" is the single most common senior JavaScript question. Cover: one thread, the two queue priorities, the fact that setTimeout(fn, 0) is a minimum delay rather than a guarantee, and that await resumes as a microtask. ================================================================================ REGEX ================================================================================ DESCRIPTION Regular expressions for search, test, match, and replace. Always know whether you wanted a literal or a compiled RegExp, and whether the g flag is on. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- const re = /\d+/g; // literal const re = new RegExp("\\d+", "g"); // from a string, escape twice ------------------------------------------------------------------------------- FLAGS ------------------------------------------------------------------------------- g global, find all i case insensitive m multiline, ^ and $ match line boundaries s dotall, . matches newline u unicode y sticky, match from lastIndex only d indices, adds match positions ------------------------------------------------------------------------------- CHARACTER CLASSES ------------------------------------------------------------------------------- . any char except newline \d \D digit / not digit \w \W word char [A-Za-z0-9_] / not \s \S whitespace / not \b \B word boundary / not [abc] any of [^abc] none of [a-z] range ------------------------------------------------------------------------------- QUANTIFIERS ------------------------------------------------------------------------------- * 0 or more + 1 or more ? 0 or 1 {n} exactly n {n,} n or more {n,m} between n and m *? +? lazy (shortest match) ------------------------------------------------------------------------------- GROUPS AND ASSERTIONS ------------------------------------------------------------------------------- (abc) capture group (?:abc) non-capturing (?abc) named group a|b alternation (?=abc) positive lookahead (?!abc) negative lookahead (?<=abc) positive lookbehind (?\d{4})-(?\d{2})-(?\d{2})/); m.groups.y; // "2026" All matches with index: for (const m of str.matchAll(/\d+/g)) { console.log(m[0], m.index); } ------------------------------------------------------------------------------- REPLACEMENT PATTERNS ------------------------------------------------------------------------------- $& the whole match $1 $2 capture groups $ named group $$ a literal dollar sign "2026-09-13".replace(/(\d+)-(\d+)-(\d+)/, "$3/$2/$1"); ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Trim whitespace: /^\s+|\s+$/g Digits only: /^\d+$/ Email (rough): /^[^\s@]+@[^\s@]+\.[^\s@]+$/ URL (rough): /^https?:\/\/\S+$/ Hex color: /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i Split on whitespace: /\s+/ Camel to kebab: s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() Escape for regex: s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Reusing a /g regex with test() or exec(). lastIndex persists between calls, so alternating calls return alternating results. Create a fresh regex or reset re.lastIndex = 0. Catastrophic backtracking from nested quantifiers like (a+)+b on a long non-matching input. This is a real denial-of-service vector when the pattern or input is user-supplied. Trying to parse HTML with a regex. Use DOMParser. Forgetting to double-escape backslashes in new RegExp("\\d"). ================================================================================ JSON ================================================================================ DESCRIPTION JSON.stringify and JSON.parse. Dates, Map, Set, undefined, functions, and cycles do not round-trip. Prefer a reviver/replacer or a real parser at boundaries. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- const str = JSON.stringify(obj); const str = JSON.stringify(obj, null, 2); // pretty, 2-space indent const obj = JSON.parse(str); ------------------------------------------------------------------------------- REPLACER AND REVIVER ------------------------------------------------------------------------------- Whitelist keys: JSON.stringify(user, ["id", "name"]); Replacer function: JSON.stringify(obj, (key, value) => key === "password" ? undefined : value); Reviver (parse dates back): JSON.parse(str, (key, value) => /^\d{4}-\d{2}-\d{2}T/.test(value) ? new Date(value) : value); ------------------------------------------------------------------------------- TOJSON ------------------------------------------------------------------------------- class User { constructor(name, secret) { this.name = name; this.secret = secret; } toJSON() { return { name: this.name }; } } JSON.stringify(new User("Jake", "x")); // {"name":"Jake"} ------------------------------------------------------------------------------- WHAT GETS LOST ------------------------------------------------------------------------------- undefined dropped from objects, becomes null in arrays functions dropped symbols dropped Date becomes an ISO string Map / Set becomes {} BigInt throws TypeError NaN / Infinity becomes null circular references throws TypeError Serializing a Map: JSON.stringify([...map]); new Map(JSON.parse(str)); ------------------------------------------------------------------------------- SAFE PARSING ------------------------------------------------------------------------------- function safeParse(str, fallback = null) { try { return JSON.parse(str); } catch { return fallback; } } JSON.parse throws SyntaxError on bad input, including on an empty string and on the string "undefined". ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using JSON round-tripping as a deep clone. Use structuredClone(). Comparing objects with JSON.stringify equality. Key order matters, so two logically equal objects can compare unequal. Forgetting that res.json() on a fetch response is itself async and throws if the body is not valid JSON (a common cause of "Unexpected token < in JSON" when the server returned an HTML error page). ================================================================================ DATE AND TIME ================================================================================ DESCRIPTION The built-in Date is a mutable instant, local-timezone by default, and painful to format. Prefer Temporal where available, or a library, for anything beyond 'now'. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- new Date() // now new Date("2026-09-13") // ISO, parsed as UTC new Date("2026-09-13T10:30:00") // no Z, parsed as LOCAL new Date(2026, 8, 13) // month is 0-indexed new Date(1757750400000) // epoch milliseconds Date.now() // epoch ms, no object allocated ------------------------------------------------------------------------------- GETTING PARTS ------------------------------------------------------------------------------- d.getFullYear() d.getUTCFullYear() d.getMonth() // 0-11 d.getDate() // 1-31 d.getDay() // 0 = Sunday d.getHours() d.getMinutes() d.getSeconds() d.getMilliseconds() d.getTime() // epoch ms d.getTimezoneOffset() // minutes, and the sign is inverted from what // you expect: UTC-8 returns 480 ------------------------------------------------------------------------------- FORMATTING ------------------------------------------------------------------------------- d.toISOString() // "2026-09-13T17:30:00.000Z", always UTC d.toLocaleDateString("en-US") d.toLocaleTimeString("en-US") d.toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: "America/Los_Angeles" }); Intl.DateTimeFormat is the right tool for anything user-facing: new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", day: "numeric" }).format(d); Relative time: const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); rtf.format(-1, "day"); // "yesterday" ------------------------------------------------------------------------------- ARITHMETIC ------------------------------------------------------------------------------- Add days: const d2 = new Date(d); d2.setDate(d2.getDate() + 7); // handles month rollover Difference in days: const days = Math.round((b - a) / 86400000); Start of day: const start = new Date(d); start.setHours(0, 0, 0, 0); Comparison: a < b // works, Date coerces to number a.getTime() === b.getTime() // equality, NOT a === b ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Month is 0-indexed in the constructor and getMonth(), but the day of month is 1-indexed. This inconsistency causes a surprising number of off-by-one-month bugs. "2026-09-13" parses as UTC midnight, but "2026-09-13T00:00:00" parses as local midnight. In a negative-offset timezone the date-only form displays as the previous day. Comparing dates with ===. It compares object references. Use getTime(). Mutating a Date accidentally. setDate, setHours, etc. all mutate in place; copy first with new Date(d). ------------------------------------------------------------------------------- NOTES ------------------------------------------------------------------------------- The Date API is widely considered broken. Temporal is the replacement (Temporal.PlainDate, Temporal.ZonedDateTime) and is shipping across engines; check availability before using it. Until then, date-fns is the common lightweight library, and Luxon for heavy timezone work. ================================================================================ DOM ================================================================================ DESCRIPTION Selecting, creating, inserting, and updating elements. Query once, mutate in memory, insert once. innerHTML is an XSS footgun if the string includes user input. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- document.querySelector(".card") // first match or null document.querySelectorAll(".card") // static NodeList document.getElementById("app") // fastest, no # prefix el.closest(".container") // nearest ancestor match el.matches(".active") // boolean el.querySelector(":scope > li") // scoped to el NodeList is not an array. Convert with [...nodes] or Array.from(nodes) to use map/filter. ------------------------------------------------------------------------------- CREATING AND INSERTING ------------------------------------------------------------------------------- const el = document.createElement("div"); el.className = "card"; el.textContent = "Hello"; parent.append(el); // end, accepts multiple + strings parent.prepend(el); // start el.before(other); // as a sibling el.after(other); el.replaceWith(other); el.remove(); parent.insertAdjacentHTML("beforeend", "

hi

"); // positions: beforebegin, afterbegin, beforeend, afterend Batch inserts with a fragment to avoid repeated reflow: const frag = document.createDocumentFragment(); for (const item of items) frag.append(renderItem(item)); list.append(frag); ------------------------------------------------------------------------------- CONTENT ------------------------------------------------------------------------------- el.textContent // all text, safe, fast el.innerText // rendered text, triggers reflow el.innerHTML // parses HTML, XSS risk with untrusted input el.outerHTML Never assign user input to innerHTML. Use textContent, or sanitize. ------------------------------------------------------------------------------- ATTRIBUTES AND PROPERTIES ------------------------------------------------------------------------------- el.getAttribute("href") el.setAttribute("href", "/x") el.removeAttribute("href") el.hasAttribute("disabled") el.dataset.userId // maps to data-user-id el.id el.value el.checked el.disabled Attributes are the HTML source; properties are the live state. For inputs, el.value is the current value while getAttribute("value") is the original default. ------------------------------------------------------------------------------- CLASSES AND STYLES ------------------------------------------------------------------------------- el.classList.add("a", "b") el.classList.remove("a") el.classList.toggle("a") el.classList.toggle("a", condition) el.classList.contains("a") el.classList.replace("a", "b") el.style.color = "red"; el.style.setProperty("--accent", "#09f"); getComputedStyle(el).color; ------------------------------------------------------------------------------- TRAVERSAL ------------------------------------------------------------------------------- el.parentElement el.children // elements only el.childNodes // includes text nodes el.firstElementChild el.lastElementChild el.nextElementSibling el.previousElementSibling ------------------------------------------------------------------------------- FORMS ------------------------------------------------------------------------------- const data = new FormData(formEl); const obj = Object.fromEntries(data); data.get("email"); form.addEventListener("submit", (e) => { e.preventDefault(); const values = Object.fromEntries(new FormData(form)); }); ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Running a script before the DOM exists. Use defer on the script tag, type="module" (deferred by default), or a DOMContentLoaded listener. Assuming querySelectorAll returns a live list. It is static; a later DOM change is not reflected. getElementsByClassName IS live, which causes the opposite surprise. Building HTML strings from user input and assigning to innerHTML. That is stored XSS. Reading layout properties (offsetHeight, getBoundingClientRect) inside a loop that also writes styles, causing layout thrashing. Batch reads, then batch writes. ================================================================================ EVENTS ================================================================================ DESCRIPTION addEventListener, the event object, bubbling vs capturing, delegation, and custom events. Prefer delegation on a parent over a listener per child. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- el.addEventListener("click", handler); el.addEventListener("click", handler, { once: true }); el.addEventListener("scroll", handler, { passive: true }); el.addEventListener("click", handler, { signal: controller.signal }); el.removeEventListener("click", handler); // same reference required The signal option is the cleanest way to remove many listeners at once: call controller.abort() and every listener registered with that signal is detached. ------------------------------------------------------------------------------- THE EVENT OBJECT ------------------------------------------------------------------------------- e.target the element that fired it e.currentTarget the element the listener is attached to e.preventDefault() cancel the default action e.stopPropagation() stop bubbling e.stopImmediatePropagation() e.key e.code keyboard e.clientX e.clientY pointer position ------------------------------------------------------------------------------- PROPAGATION ------------------------------------------------------------------------------- Capture phase window down to the target Target phase the element itself Bubble phase target back up to window (the default) Listen during capture: el.addEventListener("click", fn, { capture: true }); ------------------------------------------------------------------------------- DELEGATION ------------------------------------------------------------------------------- One listener on a parent handles any number of children, including ones added later: list.addEventListener("click", (e) => { const item = e.target.closest("li"); if (!item || !list.contains(item)) return; handleClick(item.dataset.id); }); This is the standard pattern for dynamic lists and is much cheaper than attaching a listener per row. ------------------------------------------------------------------------------- CUSTOM EVENTS ------------------------------------------------------------------------------- el.dispatchEvent(new CustomEvent("cart:add", { detail: { id: 42 }, bubbles: true })); el.addEventListener("cart:add", (e) => console.log(e.detail.id)); ------------------------------------------------------------------------------- OBSERVERS ------------------------------------------------------------------------------- IntersectionObserver element entering the viewport (lazy loading, infinite scroll) ResizeObserver element size changes MutationObserver DOM changes const io = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) load(entry.target); } }, { rootMargin: "200px" }); io.observe(el); These replace scroll and resize polling, and they do not block the main thread the way a scroll handler does. ================================================================================ FETCH AND HTTP ================================================================================ DESCRIPTION fetch returns a Promise. It only rejects on network failure; HTTP 4xx/5xx still resolve. Check res.ok, then parse the body. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- const res = await fetch("/api/users"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const users = await res.json(); ------------------------------------------------------------------------------- POST JSON ------------------------------------------------------------------------------- const res = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Jake" }) }); ------------------------------------------------------------------------------- OTHER BODY TYPES ------------------------------------------------------------------------------- Form upload (do NOT set Content-Type; the browser adds the boundary): const form = new FormData(); form.append("file", fileInput.files[0]); await fetch("/upload", { method: "POST", body: form }); URL-encoded: body: new URLSearchParams({ a: "1", b: "2" }) ------------------------------------------------------------------------------- RESPONSE ------------------------------------------------------------------------------- res.ok // true for 200-299 res.status res.statusText res.headers.get("content-type") await res.json() await res.text() await res.blob() await res.arrayBuffer() A body can be read only once. Clone first if you need it twice: const copy = res.clone(); ------------------------------------------------------------------------------- QUERY STRINGS ------------------------------------------------------------------------------- const url = new URL("/api/search", location.origin); url.searchParams.set("q", "hello world"); url.searchParams.set("page", "2"); await fetch(url); Reading them: const params = new URLSearchParams(location.search); params.get("q"); ------------------------------------------------------------------------------- A REUSABLE WRAPPER ------------------------------------------------------------------------------- async function api(path, { method = "GET", body, ...opts } = {}) { const res = await fetch(path, { method, headers: { ...(body ? { "Content-Type": "application/json" } : {}), ...opts.headers }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(10000), ...opts }); if (!res.ok) { const text = await res.text(); throw new Error(`${res.status} ${res.statusText}: ${text}`); } return res.status === 204 ? null : res.json(); } ------------------------------------------------------------------------------- STREAMING ------------------------------------------------------------------------------- const res = await fetch(url); const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; process(decoder.decode(value, { stream: true })); } ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Assuming a 404 or 500 rejects the promise. It does not. Only network failure, CORS rejection, or an abort rejects. Check res.ok. Setting Content-Type manually on a FormData body, which breaks the multipart boundary. Calling res.json() twice, which throws because the body stream is already consumed. Forgetting credentials: "include" for cross-origin cookie auth. ================================================================================ STORAGE ================================================================================ DESCRIPTION localStorage and sessionStorage hold strings, synchronously, in the origin. IndexedDB is for large structured data. Never put secrets in web storage. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- localStorage.setItem("key", JSON.stringify(value)); const value = JSON.parse(localStorage.getItem("key") ?? "null"); localStorage.removeItem("key"); localStorage.clear(); localStorage persists until cleared, ~5-10MB, synchronous sessionStorage cleared when the tab closes IndexedDB large structured data, async, transactional Cookies sent with every request, use for auth tokens Both storage APIs hold strings only; serialize objects yourself. Both throw QuotaExceededError when full, and throw in some private browsing modes, so wrap writes in try/catch. Never store secrets in localStorage; any script on the page can read it. Use httpOnly cookies for session tokens. Cross-tab sync: window.addEventListener("storage", (e) => { if (e.key === "theme") applyTheme(e.newValue); }); ================================================================================ NODE FILE IO ================================================================================ DESCRIPTION node:fs/promises is the default. Paths go through node:path. Prefer streaming for large files; readFile/writeFile for anything that fits in memory. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- import fs from "node:fs/promises"; const text = await fs.readFile("data.json", "utf8"); const buf = await fs.readFile("image.png"); // Buffer await fs.writeFile("out.txt", "content", "utf8"); await fs.appendFile("log.txt", line + "\n"); Sync versions exist (fs.readFileSync) and are appropriate for CLI startup, never inside a server request handler. ------------------------------------------------------------------------------- DIRECTORIES ------------------------------------------------------------------------------- await fs.mkdir("out/nested", { recursive: true }); await fs.readdir("src"); await fs.readdir("src", { withFileTypes: true }); // Dirent objects await fs.readdir("src", { recursive: true }); // Node 20+ await fs.rm("out", { recursive: true, force: true }); await fs.rename("a.txt", "b.txt"); await fs.copyFile("a.txt", "b.txt"); ------------------------------------------------------------------------------- CHECKING EXISTENCE ------------------------------------------------------------------------------- try { await fs.access("file.txt"); } catch { // does not exist or not accessible } Prefer just attempting the operation and handling ENOENT; checking first is a race condition. const stats = await fs.stat("file.txt"); stats.isDirectory(); stats.isFile(); stats.size; stats.mtime; ------------------------------------------------------------------------------- PATHS ------------------------------------------------------------------------------- import path from "node:path"; path.join("a", "b", "c.txt") // "a/b/c.txt" path.resolve("a", "b") // absolute path.basename("/a/b/c.txt") // "c.txt" path.basename("/a/b/c.txt", ".txt") // "c" path.extname("c.txt") // ".txt" path.dirname("/a/b/c.txt") // "/a/b" path.relative(from, to) path.sep Always build paths with path.join, never string concatenation with slashes, or it breaks on Windows. ------------------------------------------------------------------------------- STREAMS ------------------------------------------------------------------------------- Use streams for files too large to hold in memory: import { createReadStream, createWriteStream } from "node:fs"; import { pipeline } from "node:stream/promises"; import { createGzip } from "node:zlib"; await pipeline( createReadStream("big.log"), createGzip(), createWriteStream("big.log.gz") ); Line by line: import readline from "node:readline"; const rl = readline.createInterface({ input: createReadStream("big.csv") }); for await (const line of rl) { } ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Forgetting the "utf8" encoding argument, which returns a Buffer that prints as bytes rather than text. Using fs.readFileSync inside a request handler, blocking the entire event loop for every concurrent user. Concatenating paths with "/" instead of path.join. Reading a multi-gigabyte file with readFile instead of a stream, which exhausts the heap. ================================================================================ EXPRESS ================================================================================ DESCRIPTION A thin HTTP framework: app, router, middleware, request/response helpers, and error-handling middleware. Order of app.use matters. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- import express from "express"; const app = express(); app.use(express.json()); app.get("/api/users/:id", async (req, res) => { const user = await db.findUser(req.params.id); if (!user) return res.status(404).json({ error: "not found" }); res.json(user); }); app.listen(3000, () => console.log("listening on 3000")); ------------------------------------------------------------------------------- REQUEST AND RESPONSE ------------------------------------------------------------------------------- req.params.id // route parameter req.query.page // query string req.body // parsed body (needs express.json()) req.headers.authorization req.method req.path res.json(obj) res.status(201).json(obj) res.send("text") res.sendStatus(204) res.redirect("/login") res.set("X-Custom", "value") ------------------------------------------------------------------------------- MIDDLEWARE ------------------------------------------------------------------------------- function logger(req, res, next) { console.log(req.method, req.path); next(); } app.use(logger); app.use(express.static("public")); Auth middleware: function requireAuth(req, res, next) { const token = req.headers.authorization?.split(" ")[1]; if (!token) return res.status(401).json({ error: "unauthorized" }); req.user = verify(token); next(); } app.get("/me", requireAuth, (req, res) => res.json(req.user)); Order matters. Middleware runs in registration order, and anything registered after a matched route that already sent a response never runs. ------------------------------------------------------------------------------- ROUTERS ------------------------------------------------------------------------------- // routes/users.js import { Router } from "express"; const router = Router(); router.get("/", listUsers); router.post("/", createUser); export default router; // app.js app.use("/api/users", usersRouter); ------------------------------------------------------------------------------- ERROR HANDLING ------------------------------------------------------------------------------- Error middleware has four parameters and must be registered last: app.use((err, req, res, next) => { console.error(err); res.status(err.status ?? 500).json({ error: err.message }); }); In Express 4, an async handler that rejects does NOT reach this middleware. Wrap it: const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); app.get("/x", wrap(async (req, res) => { })); Express 5 forwards rejected promises automatically. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Forgetting app.use(express.json()), leaving req.body undefined on POST requests. Forgetting to call next() in middleware, hanging the request. Sending two responses ("Cannot set headers after they are sent"), usually from a missing return before res.json(). Putting the 404 handler before the routes. ================================================================================ TESTING ================================================================================ DESCRIPTION Node's built-in test runner, Vitest/Jest matchers, mocks, and DOM tests. Arrange, act, assert. Mock at the boundary, not everything. ------------------------------------------------------------------------------- NODE BUILT-IN TEST RUNNER ------------------------------------------------------------------------------- import { test, describe, before } from "node:test"; import assert from "node:assert/strict"; describe("add", () => { test("adds two numbers", () => { assert.equal(add(1, 2), 3); }); }); node --test ------------------------------------------------------------------------------- VITEST / JEST ------------------------------------------------------------------------------- import { describe, it, expect, vi, beforeEach } from "vitest"; describe("UserService", () => { beforeEach(() => vi.clearAllMocks()); it("returns a user by id", async () => { const user = await service.find(1); expect(user).toEqual({ id: 1, name: "Jake" }); }); it("throws for a missing user", async () => { await expect(service.find(999)).rejects.toThrow("not found"); }); }); ------------------------------------------------------------------------------- COMMON MATCHERS ------------------------------------------------------------------------------- expect(x).toBe(y) // Object.is, reference equality expect(x).toEqual(y) // deep structural equality expect(x).toStrictEqual(y) // also checks undefined keys, class expect(x).toBeTruthy() / toBeFalsy() expect(x).toBeNull() / toBeUndefined() / toBeDefined() expect(arr).toContain(item) expect(arr).toHaveLength(3) expect(obj).toHaveProperty("a.b") expect(fn).toThrow("message") expect(num).toBeCloseTo(0.3, 5) expect(mock).toHaveBeenCalledWith(1, 2) expect(mock).toHaveBeenCalledTimes(1) ------------------------------------------------------------------------------- MOCKING ------------------------------------------------------------------------------- const fn = vi.fn().mockResolvedValue({ id: 1 }); vi.mock("./db.js", () => ({ findUser: vi.fn(() => ({ id: 1, name: "Jake" })) })); vi.spyOn(console, "error").mockImplementation(() => {}); Fake timers: vi.useFakeTimers(); vi.advanceTimersByTime(1000); vi.useRealTimers(); ------------------------------------------------------------------------------- TESTING THE DOM ------------------------------------------------------------------------------- import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; render(); await userEvent.click(screen.getByRole("button", { name: "Save" })); expect(fn).toHaveBeenCalled(); Query by role and accessible name rather than by test id where possible; it tests what the user actually sees. ------------------------------------------------------------------------------- BEST PRACTICES ------------------------------------------------------------------------------- Arrange, act, assert. One logical assertion per test. Name tests as sentences describing behavior, not implementation. Test behavior through the public interface, not private internals. Mock at the boundary (network, database, clock), not everything. ================================================================================ COMMON ERRORS ================================================================================ DESCRIPTION The runtime and tooling errors you will actually see, with the usual cause sitting next to each message. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- TypeError: Cannot read properties of undefined (reading 'x') Something in the chain is undefined. Use optional chaining, or trace back to why it was never set. The single most common runtime error in JavaScript. TypeError: x is not a function Wrong name, wrong import shape (default vs named), or a method called on the wrong type. Often a lost this. ReferenceError: x is not defined Typo, missing import, or touching a let/const before its declaration (temporal dead zone). SyntaxError: Unexpected token < in JSON at position 0 res.json() on an HTML response. The server returned an error page or a 404 HTML document, not JSON. RangeError: Maximum call stack size exceeded Infinite recursion, a missing base case, or spreading a very large array into a function call. TypeError: Converting circular structure to JSON JSON.stringify on an object with a cycle. Use a replacer that tracks seen objects, or structuredClone for copying. TypeError: Assignment to constant variable Reassigning a const binding. Mutating its contents is fine. Cannot use import statement outside a module The file is being treated as CommonJS. Add "type": "module". ERR_REQUIRE_ESM Requiring an ESM-only package from CommonJS. Use await import(). Cannot set headers after they are sent to the client Two responses sent from one Express handler, usually a missing return. UnhandledPromiseRejection A rejected promise with no catch. In modern Node this terminates the process. Hydration failed / Text content did not match Server-rendered HTML differs from the client render. Usually Date.now(), Math.random(), or a browser-only API used during render. ================================================================================ DEBUGGING ================================================================================ DESCRIPTION console, debugger, breakpoints, and a few techniques that beat adding another log line. Source maps make this work on compiled/bundled code. ------------------------------------------------------------------------------- CONSOLE ------------------------------------------------------------------------------- console.log() console.error() console.warn() console.table(arrayOfObjects) // very underused console.dir(obj, { depth: null }) // full nesting in Node console.group() / console.groupEnd() console.time("x") / console.timeEnd("x") console.count("hits") console.trace() // stack at this point console.assert(cond, "message") Log an object with its name using shorthand: console.log({ user, index }); // { user: {...}, index: 3 } ------------------------------------------------------------------------------- DEBUGGER ------------------------------------------------------------------------------- debugger; // breakpoint when devtools is open node --inspect-brk main.js // then attach VS Code or chrome://inspect VS Code launch.json: { "type": "node", "request": "launch", "program": "${workspaceFolder}/src/main.js", "skipFiles": ["/**"] } ------------------------------------------------------------------------------- BROWSER DEVTOOLS ------------------------------------------------------------------------------- Sources breakpoints, conditional breakpoints, watch, call stack Network request/response, timing, payloads, "Copy as fetch" Performance flame chart for slow interactions Memory heap snapshots, comparing two to find leaks Application localStorage, cookies, service workers Console $0 is the selected element, $$ is querySelectorAll Break on DOM change: right-click an element -> Break on -> subtree modifications. Very effective for "what is changing this". ------------------------------------------------------------------------------- TECHNIQUES ------------------------------------------------------------------------------- Binary search the bug: comment out half the code, narrow down. Reproduce first, minimally, before trying to fix. Read the whole stack trace, not just the first line. Check the actual runtime value, not what you assume it is. In async code, log at entry and exit to see real ordering. Use a conditional breakpoint instead of a log inside a hot loop. ================================================================================ DESIGN PATTERNS ================================================================================ DESCRIPTION The patterns that show up in JavaScript apps in practice: module, factory, observer, strategy, decorator-as-HOF, repository, and Proxy. ------------------------------------------------------------------------------- MODULE ------------------------------------------------------------------------------- ESM is the module pattern now. The classic closure form still appears: const counter = (() => { let count = 0; return { inc: () => ++count, get: () => count }; })(); ------------------------------------------------------------------------------- FACTORY ------------------------------------------------------------------------------- function createUser(type) { const base = { created: Date.now() }; switch (type) { case "admin": return { ...base, role: "admin", canDelete: true }; case "guest": return { ...base, role: "guest", canDelete: false }; default: throw new Error(`unknown type: ${type}`); } } Factory functions are more idiomatic than classes in JavaScript for simple object creation; they avoid this entirely. ------------------------------------------------------------------------------- SINGLETON ------------------------------------------------------------------------------- An ESM module is already a singleton: it is evaluated once and cached. // db.js export const db = createConnection(); Anything importing db gets the same instance. A hand-rolled singleton class is almost never needed. ------------------------------------------------------------------------------- OBSERVER / PUB-SUB ------------------------------------------------------------------------------- class EventEmitter { #listeners = new Map(); on(event, fn) { if (!this.#listeners.has(event)) this.#listeners.set(event, new Set()); this.#listeners.get(event).add(fn); return () => this.off(event, fn); // unsubscribe } off(event, fn) { this.#listeners.get(event)?.delete(fn); } emit(event, ...args) { for (const fn of this.#listeners.get(event) ?? []) fn(...args); } } Node provides this as node:events. The browser provides it via EventTarget. ------------------------------------------------------------------------------- STRATEGY ------------------------------------------------------------------------------- const strategies = { creditCard: (amount) => chargeCard(amount), paypal: (amount) => chargePaypal(amount) }; function pay(method, amount) { const strategy = strategies[method]; if (!strategy) throw new Error(`unsupported: ${method}`); return strategy(amount); } In JavaScript, strategy is just a function in a lookup table. No interface or class hierarchy required. ------------------------------------------------------------------------------- DECORATOR (HIGHER ORDER FUNCTION) ------------------------------------------------------------------------------- const withLogging = (fn) => (...args) => { console.log(`calling ${fn.name}`, args); const result = fn(...args); console.log(`returned`, result); return result; }; const add = withLogging((a, b) => a + b); ------------------------------------------------------------------------------- REPOSITORY ------------------------------------------------------------------------------- export function createUserRepo(db) { return { findById: (id) => db.query("SELECT * FROM users WHERE id = $1", [id]), create: (user) => db.query("INSERT INTO users ...", [user]), }; } Taking db as a parameter is dependency injection without a container, and makes the repo trivially testable with a fake db. ------------------------------------------------------------------------------- PROXY ------------------------------------------------------------------------------- const validated = new Proxy(target, { set(obj, prop, value) { if (prop === "age" && typeof value !== "number") { throw new TypeError("age must be a number"); } obj[prop] = value; return true; } }); Proxy powers Vue's reactivity and MobX. Rarely needed in application code, but worth recognizing. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Importing class-heavy Java or C# patterns into JavaScript where a closure or a plain function would do. An abstract factory of strategy classes is usually an object of functions here. ================================================================================ BEST PRACTICES ================================================================================ DESCRIPTION Naming, modern idioms, performance, memory, security, and the interview questions that recycle the rest of this file. ------------------------------------------------------------------------------- NAMING CONVENTIONS ------------------------------------------------------------------------------- camelCase variables, functions, methods, properties PascalCase classes, constructors, React components UPPER_SNAKE true constants (config values, magic numbers) #private private class fields _private legacy convention, advisory only kebab-case file names, CSS classes, HTML attributes isX / hasX booleans getX / fetchX sync retrieval vs network retrieval onX / handleX event props vs event handlers ------------------------------------------------------------------------------- MODERN IDIOMS ------------------------------------------------------------------------------- const by default, let when reassigning, never var. === always; == only for the x == null idiom. ?? for defaults, ?. for optional access. Destructure parameters instead of long positional lists. Prefer immutable array methods (toSorted, with, toSpliced). Prefer for-of over forEach when you might need to break or await. Prefer early returns (guard clauses) over deep nesting. Prefer async/await over .then chains. Prefer named exports over default exports for refactorability. ------------------------------------------------------------------------------- PERFORMANCE ------------------------------------------------------------------------------- Avoid shift()/unshift() in loops; they are O(n). Avoid spreading inside a loop to build an array; push instead. Use a Set or Map for membership checks instead of includes() on a large array in a loop, turning O(n^2) into O(n). Batch DOM writes; avoid interleaving reads and writes (layout thrashing). Debounce input handlers and throttle scroll handlers. Use IntersectionObserver instead of scroll listeners. Move CPU-heavy work to a Web Worker or worker_threads. Lazy-load with dynamic import() for code splitting. Measure before optimizing; devtools Performance panel over intuition. ------------------------------------------------------------------------------- MEMORY ------------------------------------------------------------------------------- Common leak sources: forgotten event listeners, setInterval never cleared, closures capturing large objects, detached DOM nodes still referenced by an array, and growing caches with no eviction. Use WeakMap or WeakSet when keying metadata by object identity so the entry disappears with the object. Clear timers and listeners on teardown (React useEffect cleanup, AbortController signals, removeEventListener). ------------------------------------------------------------------------------- SECURITY ------------------------------------------------------------------------------- Never assign untrusted input to innerHTML. Use textContent. Never store tokens in localStorage; use httpOnly, SameSite cookies. Never build SQL with string concatenation; use parameterized queries. Validate and sanitize on the server; client validation is UX only. Do not put secrets in front-end env vars; anything shipped is public. Keep dependencies updated and run npm audit. Set a Content-Security-Policy header. ------------------------------------------------------------------------------- CLEAN CODE ------------------------------------------------------------------------------- Keep functions short and single-purpose. Name things so comments explaining "what" become unnecessary; reserve comments for "why". Avoid boolean parameters that make call sites unreadable; pass an options object instead. Return early on invalid input. Prefer pure functions where practical; isolate side effects. Do not abstract until the third repetition. ------------------------------------------------------------------------------- COMMON INTERVIEW QUESTIONS ------------------------------------------------------------------------------- Explain the event loop, microtasks vs macrotasks. Explain closures, with a real use case. Explain how this is determined, and the four binding rules. Explain prototypal inheritance and what new does. Difference between == and ===, and the falsy values. Difference between var, let, and const, and the temporal dead zone. Difference between map, forEach, and reduce. Explain Promise.all vs allSettled vs race vs any. Implement debounce and throttle. Implement a deep clone. Implement Promise.all. Explain event delegation and why it is used. Explain CORS and what triggers a preflight request. Shallow vs deep copy. What is the difference between null and undefined. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Treating JavaScript as a class-first language because of a C# or Java background. Functions, closures, and plain objects carry most of the weight here; classes are one tool among several. Reaching for a library for something the platform now does natively (fetch, structuredClone, Object.groupBy, Intl, AbortController). Optimizing without measuring, and micro-optimizing loops while the real cost is a network waterfall or an unnecessary re-render. ================================================================================ LEETCODE PROBLEMS ================================================================================ DESCRIPTION A handful of canonical problems with the JavaScript solution you should be able to write from memory. Space for solved problems, in the same spirit as the C# sheet. Keep the pattern name in the header so grep finds it by technique as well as by problem name. ------------------------------------------------------------------------------- TWO SUM (HASH MAP) ------------------------------------------------------------------------------- var twoSum = function (nums, target) { const seen = new Map(); for (let i = 0; i < nums.length; i++) { const need = target - nums[i]; if (seen.has(need)) return [seen.get(need), i]; seen.set(nums[i], i); } return []; }; ------------------------------------------------------------------------------- VALID ANAGRAM (COUNT MAP) ------------------------------------------------------------------------------- var isAnagram = function (s, t) { if (s.length !== t.length) return false; const counts = new Map(); for (const c of s) counts.set(c, (counts.get(c) ?? 0) + 1); for (const c of t) { const n = counts.get(c); if (!n) return false; counts.set(c, n - 1); } return true; }; ------------------------------------------------------------------------------- BEST TIME TO BUY AND SELL STOCK (GREEDY) ------------------------------------------------------------------------------- var maxProfit = function (prices) { let min = Infinity, best = 0; for (const p of prices) { min = Math.min(min, p); best = Math.max(best, p - min); } return best; }; ------------------------------------------------------------------------------- MERGE INTERVALS (SORT) ------------------------------------------------------------------------------- var merge = function (intervals) { intervals.sort((a, b) => a[0] - b[0]); const out = []; for (const [start, end] of intervals) { const last = out.at(-1); if (last && start <= last[1]) last[1] = Math.max(last[1], end); else out.push([start, end]); } return out; }; ================================================================================ END OF FILE ================================================================================