javascript.md

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));

var is function-scoped and hoisted as undefined; treat it as legacy and reach for const first, let only 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 this from the surrounding scope; regular functions get this from 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 || 10 replaces a legitimate 0, while count ?? 10 only 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),
});

fetch only rejects on network failure or abort. An HTTP error status still resolves, so check res.ok yourself.

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 Map over a plain object for dynamic keys, and WeakMap for per-object data that should not block garbage collection.

Common gotchas #

ExpressionResultUse instead
0 == ""true=== and !==, always
NaN === NaNfalseNumber.isNaN(x)
typeof null'object'x === null
0.1 + 0.2 === 0.3falseepsilon compare, or integer cents
x before let x runsReferenceError (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

© 2026 Anguished LLC · anguishedturtle.com · About · Bit Night Runner · Support · Privacy