JavaScript #
Modern syntax, async patterns, DOM work, and the gotchas worth memorizing.
Values & coercion #
typeof 42 // 'number' (one numeric type; bigint is separate)
typeof null // 'object' (historic bug, kept forever)
Boolean("") // false; also 0, -0, NaN, null, undefined, 0n
"5" + 1 // '51' (+ prefers string concatenation)
"5" - 1 // 4 (- coerces both sides to numbers)
Number("12px") // NaN; Number("") is 0
1 === "1" // false; === never coerces, use it everywhere
let, const & scope #
const xs = [1, 2]; // the binding is fixed, the contents are not
xs.push(3); // fine; xs = [] would throw
let count = 0; // block-scoped, reassignable
{ let inner = 1; } // gone outside the braces
for (let i = 0; i < 3; i++) // fresh i per iteration: callbacks see 0 1 2
queue(() => log(i));
varis function-scoped and hoisted asundefined; treat it as legacy and reach forconstfirst,letonly when you reassign.
Template literals #
const name = "turtle";
`hi ${name}, ${1 + 2}` // any expression interpolates -> hi turtle, 3
`line one
line two` // real newlines, no \n escapes needed
const row = `<li>${escapeHtml(user)}</li>`; // escape untrusted input yourself
String.raw`C:\new\table` // backslashes kept literally
Destructuring & spread #
const [first, ...rest] = [1, 2, 3]; // first=1, rest=[2, 3]
const { id, name: label = "?" } = obj; // pick, rename, default
const copy = [...xs, 4]; // shallow copy plus append
const merged = { ...base, ...extra }; // later keys win
function draw({ x = 0, y = 0 } = {}) {} // named optional arguments
[a, b] = [b, a]; // swap without a temp
Arrow functions & this #
const add = (a, b) => a + b; // implicit return
const wrap = x => ({ value: x }); // parenthesize a returned object literal
button.addEventListener("click", () => this.open());
Arrows capture
thisfrom the surrounding scope; regular functions getthisfrom the call site. Use arrows for callbacks, and method shorthand (open() {}) for object and class methods.
Array methods #
xs.map(x => x * 2) // transform every element
xs.filter(x => x > 0) // keep matches
xs.reduce((sum, x) => sum + x, 0) // fold to a single value
xs.find(x => x.id === 7) // first match or undefined
xs.some(x => x > 9) // any match -> boolean
xs.every(x => x > 0) // all match -> boolean
[1, [2, [3]]].flat(Infinity) // -> [1, 2, 3]
xs.at(-1) // last element (ES2022)
Objects #
const x = 1, y = 2;
const p = { x, y, dist() { return Math.hypot(this.x, this.y); } };
user?.address?.city // undefined instead of a TypeError
user.nickname ?? "anon" // default only for null/undefined, not 0 or ""
Object.entries(p) // -> [['x', 1], ['y', 2], ...]; keys/values too
const deep = structuredClone(state); // true deep copy: Dates, Maps, cycles
??beats||for defaults:count || 10replaces a legitimate 0, whilecount ?? 10only replaces null and undefined.
Classes & prototypes #
class Counter {
#count = 0; // private field, invisible outside
get value() { return this.#count; }
increment() { this.#count += 1; return this; }
static from(n) { const c = new Counter(); c.#count = n; return c; }
}
class Stepper extends Counter {} // instanceof both classes
Classes are syntax over prototypes: methods live once on
Counter.prototype, and property lookups walk the prototype chain.
Closures #
function makeCounter() {
let n = 0; // captured, effectively private
return () => ++n;
}
const next = makeCounter();
next(); next(); // -> 2; both calls share the same n
Every function keeps a live reference to the scope it was created in. That is how callbacks remember state long after the outer call returned.
Modules #
export const MAX = 10; // named export
export default function run() {} // one default per file
import run, { MAX } from "./run.js"; // default + named together
import * as util from "./util.js"; // namespace object
const { chart } = await import("./chart.js"); // lazy, loads on demand
A module is a singleton: every importer sees the same instance, which makes module scope a natural home for shared state.
Promises & async/await #
async function load(url) {
try {
const data = await getJson(url); // pauses this function, never the page
return data;
} catch (e) { // rejected promise lands here
report(e);
}
}
await Promise.all([a(), b()]); // parallel; rejects on the first failure
await Promise.allSettled([a(), b()]); // always resolves, one status per input
p.then(ok).catch(err).finally(done); // the underlying chain
Fetch with error handling #
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`); // fetch resolves on 404s
const data = await res.json();
await fetch(url, { // POST with a JSON body
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
fetchonly rejects on network failure or abort. An HTTP error status still resolves, so checkres.okyourself.
JSON #
JSON.stringify(obj, null, 2) // pretty-printed string
JSON.parse(text) // throws on malformed input, wrap in try
JSON.stringify({ f() {}, u: undefined }) // -> '{}': both are silently dropped
JSON.parse(JSON.stringify(x)) // old deep-copy trick; prefer structuredClone
DOM selection & events #
const panel = document.querySelector("#panel"); // first match, any selector
[...document.querySelectorAll(".card")] // spread to use array methods
panel.addEventListener("click", onClick, { once: true });
panel.classList.toggle("open"); // add / remove / contains too
panel.dataset.userId // reads data-user-id
list.addEventListener("click", (e) => { // delegation: one listener
const item = e.target.closest("li[data-id]"); // covers future children too
if (item) select(item.dataset.id);
});
Timers & rAF #
const id = setTimeout(tick, 1000); // once, after roughly 1s
clearTimeout(id);
setInterval(poll, 5000); // repeats; chained timeouts guarantee a gap
requestAnimationFrame(function frame(t) {
draw(t); // runs once per display frame
requestAnimationFrame(frame); // loop; throttled in background tabs
});
Map & Set #
const seen = new Set([1, 2, 2]); // -> Set {1, 2}
seen.has(2); seen.add(3);
const unique = [...new Set(xs)]; // dedupe an array in one line
const m = new Map([[el, state]]); // any key type, insertion order kept
m.get(el); m.set(el, next); m.size;
Prefer
Mapover a plain object for dynamic keys, andWeakMapfor per-object data that should not block garbage collection.
Common gotchas #
| Expression | Result | Use instead |
|---|---|---|
0 == "" | true | === and !==, always |
NaN === NaN | false | Number.isNaN(x) |
typeof null | 'object' | x === null |
0.1 + 0.2 === 0.3 | false | epsilon compare, or integer cents |
x before let x runs | ReferenceError (the TDZ) | declare before use |
[10, 1, 2].sort() | [1, 10, 2] (lexicographic) | sort((a, b) => a - b) |
parseInt("08px") | 8 (stops at px) | Number(s) for strict parsing |