JavaScript cheatsheet
Save this file from here if you want a copy.
================================================================================
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 +<line number> ~/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
-------------------------------------------------------------------------------
<script type="module" src="./main.js"></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
(?<name>abc) named group
a|b alternation
(?=abc) positive lookahead
(?!abc) negative lookahead
(?<=abc) positive lookbehind
(?<!abc) negative lookbehind
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
re.test(str) // boolean
re.exec(str) // match object or null, stateful with /g
str.match(re) // array or null
str.matchAll(re) // iterator, requires /g
str.replace(re, repl)
str.replaceAll(re, repl) // regex must have /g
str.split(re)
str.search(re) // index or -1
Named groups:
const m = "2026-09-13".match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\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
$<name> 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", "<p>hi</p>");
// 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<Response>. 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(<Button onClick={fn}>Save</Button>);
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": ["<node_internals>/**"]
}
-------------------------------------------------------------------------------
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
================================================================================