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
================================================================================