Table of Contents

0. Setup & Environment

Install Node.js via Homebrew

The most common approach on macOS. Node.js is the runtime that lets you execute JavaScript outside the browser — required for any server-side JS, tooling, or build pipelines.

# Install Node.js + npm in one step
brew install node

# Verify
node --version   # e.g. v22.x.x
npm --version    # e.g. 10.x.x

Alternative: nvm (Recommended for Most Developers)

nvm (Node Version Manager) lets you install and switch between multiple Node versions — essential when different projects require different runtimes.

# Install nvm via Homebrew (or via the install script below)
brew install nvm

# Or install via the official script
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash

# Add to ~/.zshrc (Homebrew path):
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"

# Install, activate, and pin a version
nvm install 22       # download Node 22 LTS
nvm use 22           # activate in current shell
nvm alias default 22 # make it the default for new shells

# List installed versions
nvm ls
Why nvm over Homebrew Node?
Homebrew installs a single global Node version. With nvm you can run nvm use 18 in one project and nvm use 22 in another without touching global state. Pair it with an .nvmrc file in your repo root (echo "22" > .nvmrc) and teammates get the right version automatically.

Package Managers

npm ships with Node and is the default. Two popular alternatives offer speed and workspace improvements:

# npm (built-in) — initialize a project
npm init -y               # create package.json with defaults
npm install lodash        # add a dependency
npm install --save-dev jest  # add a dev dependency
npm run <script>          # run a script defined in package.json

# yarn — faster installs, workspaces support
npm install -g yarn
yarn add lodash

# pnpm — disk-efficient (hard-links shared packages)
npm install -g pnpm
pnpm add lodash
Which package manager to use?
Start with npm — it's always available and well-documented. Switch to pnpm if you work in a monorepo or care about disk space. Use yarn if an existing project already requires it. Don't mix them within the same project (check for a lockfile: package-lock.json = npm, yarn.lock = yarn, pnpm-lock.yaml = pnpm).

Browser DevTools & REPL

Two zero-friction environments for experimenting with JavaScript:

# Node.js REPL — interactive JS in the terminal
node
> 2 ** 10
1024
> [1,2,3].map(x => x * x)
[ 1, 4, 9 ]
> .exit

In Chrome or Safari, open DevTools (Cmd+Option+I) and use the Console tab for the same experience in a browser context — with access to document, fetch, and the full Web API surface.

Editor Setup

VS Code has first-class JavaScript and TypeScript support built in. Two extensions are effectively mandatory on any real project:

  • ESLint (dbaeumer.vscode-eslint) — surfaces lint errors inline as you type.
  • Prettier (esbenp.prettier-vscode) — opinionated auto-formatter; eliminates style debates.
Enable format-on-save
Add "editor.formatOnSave": true to your VS Code settings.json. Prettier will reformat every time you save — you'll never think about semicolons or trailing commas again.

Quick Verify

Confirm your environment is working end-to-end:

mkdir ~/js-refresher && cd ~/js-refresher
npm init -y
echo 'console.log("Hello, JavaScript!")' > index.js
node index.js
# Hello, JavaScript!

If you see the greeting, Node.js is installed correctly and your first module is running.

1. Language Fundamentals

The 8 Primitive Types

JavaScript has 7 primitive types plus object (the only structural type):

Typetypeof resultExample valuesNotes
undefined"undefined"undefinedUninitialized variable or missing property
null"object"nullHistoric bug — null is NOT an object
boolean"boolean"true, false
number"number"42, 3.14, NaN, Infinity64-bit IEEE 754 float
bigint"bigint"9007199254740993nArbitrary precision integers (ES2020)
string"string""hello", 'world', `template`Immutable UTF-16 sequences
symbol"symbol"Symbol("id")Unique, non-enumerable keys (ES2015)
object"object"{}, [], nullAll non-primitive values + null (bug)

typeof Quirks

// The famous null bug
typeof null === "object"        // true — historical bug, never fixed

// Functions have their own typeof
typeof function(){} === "function"  // true — though functions are objects

// Undeclared variables don't throw with typeof
typeof undeclaredVar === "undefined"  // true — safe to use as feature detection

// NaN is a number
typeof NaN === "number"   // true
Number.isNaN(NaN)         // true — use this instead of global isNaN()

// Arrays are objects
typeof [] === "object"    // true
Array.isArray([])         // true — use this to detect arrays

// BigInt
typeof 42n === "bigint"   // true

// Symbol
typeof Symbol() === "symbol"  // true

Type Coercion

JavaScript uses three abstract operations for coercion. Understanding them explains most "wat" behavior:

ToPrimitive

Converts an object to a primitive. Takes an optional hint ("number", "string", or "default"):

  1. If the object has [Symbol.toPrimitive], call it with the hint.
  2. For "string" hint: try toString() then valueOf().
  3. For "number"/"default" hint: try valueOf() then toString().
const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return 42;
    if (hint === 'string') return 'forty-two';
    return true; // default hint
  }
};

+obj          // 42    (number hint)
`${obj}`      // "forty-two" (string hint)
obj + ""      // "true"  (default hint, then string concat)

// Default valueOf returns the object itself (not primitive)
// Default toString returns "[object Object]"
const plain = {};
plain + 1     // "[object Object]1"  — valueOf → object, toString → "[object Object]"

ToNumber

Number(undefined)   // NaN
Number(null)        // 0        ← surprising
Number(true)        // 1
Number(false)       // 0
Number("")          // 0        ← surprising
Number("  ")        // 0        ← whitespace-only strings → 0
Number("42")        // 42
Number("0x1F")      // 31       (hex)
Number("0b1010")    // 10       (binary, ES6)
Number("0o17")      // 15       (octal, ES6)
Number("42abc")     // NaN      (parseInt would give 42)
Number([])          // 0        ← [] → "" → 0
Number([3])         // 3        ← [3] → "3" → 3
Number([1,2])       // NaN      ← [1,2] → "1,2" → NaN
Number({})          // NaN

ToString

String(undefined)   // "undefined"
String(null)        // "null"
String(true)        // "true"
String(false)       // "false"
String(0)           // "0"
String(-0)          // "0"       ← -0 loses sign
String(NaN)         // "NaN"
String(Infinity)    // "Infinity"
String([1,2,3])     // "1,2,3"   ← Array.join(',')
String({})          // "[object Object]"

== vs === Comparison

Danger: Abstract Equality is a minefield
Use === (strict equality) by default. Only use == when you explicitly want type coercion, which is almost never.
// === strict: same type AND same value
1 === 1           // true
1 === "1"         // false — different types
null === undefined // false

// == abstract: coerces types (the algorithm is complex)
null == undefined  // true  ← only these two are == to each other (not 0, "", false)
0 == ""           // true  ← both ToNumber → 0
0 == false        // true  ← false → 0
"" == false       // true  ← both → 0
"0" == false      // true  ← "0" → 0, false → 0
[] == false       // true  ← [] → 0, false → 0
[] == ![]         // true  ← ![] is false, then [] == false
NaN == NaN        // false ← NaN is never equal to anything, use Number.isNaN()

// Object == primitive: object is converted via ToPrimitive
[1] == 1          // true  ← [1] → "1" → 1
[1,2] == "1,2"    // true  ← [1,2] → "1,2"

// Object == object: reference equality only
{} == {}          // false — different references
const a = {};
a == a            // true  — same reference

Truthy and Falsy

// The 8 falsy values — everything else is truthy
false
0
-0
0n            // BigInt zero
""            // empty string
null
undefined
NaN

// Commonly mistaken truthy values
"0"           // truthy — non-empty string
"false"       // truthy
[]            // truthy — empty array
{}            // truthy — empty object
new Boolean(false)  // truthy — objects are always truthy

Scope

// Global scope: window (browser) or globalThis (universal)
var globalVar = "I'm global";
globalThis.globalVar;   // accessible anywhere

// Function scope: var is scoped to the nearest function
function example() {
  var funcScoped = "only inside this function";
  if (true) {
    var alsoFuncScoped = "var ignores blocks";
  }
  console.log(alsoFuncScoped);  // "var ignores blocks" — no block scope
}

// Block scope: let and const are scoped to { }
{
  let blockScoped = "only in this block";
  const alsoBlock = "also block scoped";
}
// console.log(blockScoped);  // ReferenceError

// Lexical scope: inner functions access outer variables
function outer() {
  const x = 10;
  function inner() {
    console.log(x);  // 10 — closes over outer's x
  }
  return inner;
}

// Scope chain lookup order:
// 1. Current scope
// 2. Enclosing function scopes (lexical, not dynamic)
// 3. Module scope
// 4. Global scope

2. Variables & Declarations

var vs let vs const

Featurevarletconst
ScopeFunction / GlobalBlockBlock
HoistingHoisted + initialized to undefinedHoisted, NOT initialized (TDZ)Hoisted, NOT initialized (TDZ)
Re-declarationAllowedError in same scopeError in same scope
Re-assignmentAllowedAllowedError
Global propertyYes (window.x)NoNo
Use in loopsShared across iterations (closure bug)New binding per iterationPer iteration (but can't reassign)

Hoisting in Detail

// var: hoisted and initialized to undefined
console.log(x);  // undefined — not ReferenceError
var x = 5;
// The engine sees it as:
// var x;  ← hoisted to top of function
// console.log(x);
// x = 5;

// Function declarations: fully hoisted (name + body)
greet();          // "Hello" — works before declaration
function greet() { console.log("Hello"); }

// Function expressions: only the variable is hoisted
// sayHi();      // TypeError: sayHi is not a function
var sayHi = function() { console.log("Hi"); };

// let/const: hoisted but NOT initialized — Temporal Dead Zone
// console.log(y);  // ReferenceError: Cannot access 'y' before initialization
let y = 10;

// Class declarations: also in TDZ
// new MyClass();   // ReferenceError
class MyClass {}

Temporal Dead Zone (TDZ)

What is the TDZ?
The TDZ is the period between entering a scope (where the binding is hoisted) and the line where the variable is actually declared. Accessing a let or const variable during its TDZ throws a ReferenceError.
// TDZ applies even if the name exists in outer scope
let x = "outer";
{
  // TDZ starts here for block-level x
  console.log(x);  // ReferenceError — inner x is hoisted but in TDZ
  let x = "inner"; // TDZ ends here
}

// TDZ with typeof — unlike undeclared vars, typeof in TDZ also throws
{
  typeof blockVar;  // ReferenceError (not "undefined" like undeclared)
  let blockVar = 1;
}

const with Objects and Arrays

const is shallow, not deep
const prevents re-assignment of the binding, but does NOT prevent mutation of the value if it's an object or array.
const obj = { x: 1 };
obj.x = 2;          // OK — mutating the object
obj.y = 3;          // OK — adding property
// obj = {};        // TypeError: Assignment to constant variable

const arr = [1, 2, 3];
arr.push(4);        // OK — mutating the array
arr[0] = 99;        // OK
// arr = [];        // TypeError

// For deep immutability, use Object.freeze (shallow) or a library
const frozen = Object.freeze({ a: 1, nested: { b: 2 } });
frozen.a = 99;         // Silently ignored (throws in strict mode)
frozen.nested.b = 99;  // Works! Object.freeze is only 1 level deep

// Deep freeze
function deepFreeze(obj) {
  Object.getOwnPropertyNames(obj).forEach(name => {
    const value = obj[name];
    if (typeof value === 'object' && value !== null) {
      deepFreeze(value);
    }
  });
  return Object.freeze(obj);
}

3. Functions

Declarations vs Expressions vs Arrow Functions

// Function declaration — hoisted, has own `this`, `arguments`, `new.target`
function add(a, b) { return a + b; }

// Named function expression — not hoisted, name visible only inside itself
const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);  // fact available here
};
// fact(5);  // ReferenceError — not in outer scope

// Arrow function — NOT hoisted, NO own `this`/`arguments`/`prototype`
const multiply = (a, b) => a * b;          // implicit return
const square = x => x * x;                 // single param, no parens needed
const getObj = () => ({ key: "value" });   // returning object literal — wrap in ()
const multi = (x) => {
  const result = x * 2;
  return result;                           // explicit return with braces
};

Arrow Function `this` Binding

Key distinction: lexical vs dynamic `this`
Arrow functions capture this from their enclosing lexical scope at definition time. Regular functions receive this dynamically at call time.
class Timer {
  constructor() {
    this.seconds = 0;
  }

  // Problem with regular function: `this` is lost in callback
  startBroken() {
    setInterval(function() {
      this.seconds++;  // `this` is undefined (strict) or window (sloppy)
    }, 1000);
  }

  // Arrow function captures `this` from Timer instance
  startFixed() {
    setInterval(() => {
      this.seconds++;  // `this` correctly refers to Timer instance
    }, 1000);
  }

  // Alternative: bind
  startBound() {
    setInterval(function() {
      this.seconds++;
    }.bind(this), 1000);
  }
}

// Arrow functions cannot be used as methods when you need dynamic `this`
const obj = {
  name: "Alice",
  greetArrow: () => `Hi ${this.name}`,    // `this` is outer scope (not obj)
  greetMethod() { return `Hi ${this.name}`; }  // correct: dynamic this
};

obj.greetArrow();   // "Hi undefined" (or window.name)
obj.greetMethod();  // "Hi Alice"

Closures

A closure is a function that retains access to its lexical scope even when called outside that scope. This is a fundamental JS mechanism, not a special feature.

// Basic closure: counter with private state
function makeCounter(initial = 0) {
  let count = initial;  // private — not accessible from outside

  return {
    increment() { count++; },
    decrement() { count--; },
    value() { return count; }
  };
}

const counter = makeCounter(10);
counter.increment();
counter.increment();
counter.value();  // 12

// Closure for memoization
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  // simulate expensive computation
  return n * n;
});

// Closure for partial application
function multiply(a) {
  return (b) => a * b;  // closes over `a`
}
const double = multiply(2);
const triple = multiply(3);
double(5);   // 10
triple(5);   // 15

IIFE (Immediately Invoked Function Expression)

// Classic IIFE — used pre-modules to create private scope
(function() {
  const privateVar = "hidden from global scope";
  // ... code ...
})();

// Arrow function IIFE
(() => {
  // ...
})();

// Named IIFE — useful for stack traces
(function init() {
  // ...
})();

// IIFE with return value
const result = (function() {
  return 42;
})();

// Modern alternative: block scoping with let/const
{
  const privateVar = "also hidden";
  // ...
}

Default, Rest, and Spread

// Default parameters — evaluated at call time, not definition time
function createTag(tag = "div", cls = `default-${tag}`) {
  return `<${tag} class="${cls}">`;
}
createTag();              // 
createTag("p"); //

createTag("p", undefined) //

— undefined triggers default createTag("p", null) //

— null does NOT trigger default // Default with destructuring function connect({ host = "localhost", port = 3000, tls = false } = {}) { return { host, port, tls }; } connect(); // { host: "localhost", port: 3000, tls: false } connect({ port: 8080 }); // { host: "localhost", port: 8080, tls: false } // Rest parameters — collects remaining args into an array function sum(first, ...rest) { return rest.reduce((acc, n) => acc + n, first); } sum(1, 2, 3, 4); // 10 // Spread in function calls const nums = [1, 2, 3]; Math.max(...nums); // 3 Math.max(...nums, 10, ...nums); // 10 // Spread is not the same as .apply() // apply passes `this`, spread just unpacks fn.apply(ctx, nums); // passes ctx as this fn(...nums); // this determined by call site

Generator Functions

// function* — can pause and resume execution
function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;  // pause and emit a value
  }
}

const gen = range(0, 10, 2);
gen.next();  // { value: 0, done: false }
gen.next();  // { value: 2, done: false }
// ...
gen.next();  // { value: undefined, done: true }

// Generators are iterators — use in for...of
for (const n of range(0, 5)) {
  console.log(n);  // 0, 1, 2, 3, 4
}

// yield* — delegate to another iterable
function* concat(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}
[...concat([1, 2], [3, 4])];  // [1, 2, 3, 4]

// Two-way communication: gen.next(value) sends value back into generator
function* accumulator() {
  let total = 0;
  while (true) {
    const value = yield total;   // yields current total, receives next value
    if (value === null) break;
    total += value;
  }
  return total;
}

const acc = accumulator();
acc.next();      // { value: 0, done: false }   — start
acc.next(10);    // { value: 10, done: false }
acc.next(20);    // { value: 30, done: false }
acc.next(null);  // { value: 30, done: true }   — final return

Async Generators

// async function* — combine generators with async/await
async function* fetchPages(baseUrl) {
  let page = 1;
  while (true) {
    const res = await fetch(`${baseUrl}?page=${page}`);
    const data = await res.json();
    if (data.items.length === 0) break;
    yield data.items;
    page++;
  }
}

// Consume with for await...of
async function processAll(url) {
  for await (const items of fetchPages(url)) {
    for (const item of items) {
      console.log(item);
    }
  }
}

// Practical: streaming from a ReadableStream
async function* streamLines(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();           // keep incomplete line
    for (const line of lines) {
      yield line;
    }
  }
  if (buffer) yield buffer;
}

4. Objects & Prototypes

Object Literals

const name = "Alice";
const age = 30;

// ES6+ shorthand syntax
const person = {
  name,              // shorthand: name: name
  age,               // shorthand: age: age

  // Method shorthand (has own `arguments`, `new.target`, different from arrow)
  greet() {
    return `Hi, I'm ${this.name}`;
  },

  // Computed property names
  [`prop_${1 + 1}`]: "computed",  // { prop_2: "computed" }

  // Getters and setters
  get fullName() {
    return `${this.name} Smith`;
  },
  set fullName(val) {
    this.name = val.split(" ")[0];
  }
};

// Object spread (ES2018) — shallow copy + override
const updated = { ...person, age: 31, city: "NYC" };

// Object.assign — same as spread but mutates target
const merged = Object.assign({}, person, { age: 31 });

The Prototype Chain

// Every object has [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf)
const arr = [1, 2, 3];
// arr → Array.prototype → Object.prototype → null

// Property lookup walks the chain:
arr.push(4);          // found on Array.prototype
arr.hasOwnProperty('0');  // found on Object.prototype

// Object.create — create object with specified prototype
const animal = {
  speak() { return `${this.name} makes a noise.`; }
};

const dog = Object.create(animal);
dog.name = "Rex";
dog.speak();          // "Rex makes a noise."

// Check chain
Object.getPrototypeOf(dog) === animal;  // true
dog.hasOwnProperty("name");             // true
dog.hasOwnProperty("speak");            // false — on prototype

// Object.create(null) — no prototype at all (pure dictionary)
const dict = Object.create(null);
dict.hasOwnProperty;  // undefined — no inherited methods, safer for maps

class Syntax

Classes are syntactic sugar over prototype-based inheritance. The underlying mechanism is unchanged.

class Animal {
  // Class field (ES2022)
  #name;             // private field — enforced by the engine
  static count = 0;  // static class field

  constructor(name, sound) {
    this.#name = name;
    this.sound = sound;
    Animal.count++;
  }

  // Instance method — on Animal.prototype
  speak() {
    return `${this.#name} says ${this.sound}`;
  }

  // Getter/setter
  get name() { return this.#name; }
  set name(val) {
    if (typeof val !== 'string') throw new TypeError('Name must be string');
    this.#name = val;
  }

  // Static method — on Animal itself, not instances
  static create(name, sound) {
    return new Animal(name, sound);
  }

  // Private method (ES2022)
  #validate() {
    return this.#name.length > 0;
  }

  toString() {
    return `Animal(${this.#name})`;
  }
}

class Dog extends Animal {
  #tricks = [];  // private field with initializer

  constructor(name) {
    super(name, "woof");  // must call super() before `this`
  }

  learn(trick) {
    this.#tricks.push(trick);
    return this;  // fluent/chainable
  }

  // Override parent method
  speak() {
    const base = super.speak();  // call parent
    return `${base}! Tricks: ${this.#tricks.join(", ") || "none"}`;
  }
}

const d = new Dog("Rex");
d.learn("sit").learn("shake");
d.speak();  // "Rex says woof! Tricks: sit, shake"
d instanceof Dog;     // true
d instanceof Animal;  // true

Mixins Pattern

When to use mixins
JavaScript only supports single inheritance. Mixins let you compose behavior from multiple sources without deep class hierarchies.
// Mixin: a function that takes a base class and returns an extended class
const Serializable = (Base) => class extends Base {
  serialize() {
    return JSON.stringify(this);
  }
  static deserialize(json) {
    return Object.assign(new this(), JSON.parse(json));
  }
};

const Validatable = (Base) => class extends Base {
  validate() {
    return Object.keys(this).every(k => this[k] !== null);
  }
};

// Compose mixins
class User extends Serializable(Validatable(class {})) {
  constructor(name, email) {
    super();
    this.name = name;
    this.email = email;
  }
}

const u = new User("Alice", "[email protected]");
u.validate();   // true
u.serialize();  // '{"name":"Alice","email":"[email protected]"}'

5. Destructuring & Spread

Array Destructuring

const [a, b, c] = [1, 2, 3];
// a=1, b=2, c=3

// Skip elements with holes
const [first, , third] = [1, 2, 3];
// first=1, third=3

// Rest element — must be last
const [head, ...tail] = [1, 2, 3, 4, 5];
// head=1, tail=[2,3,4,5]

// Default values
const [x = 10, y = 20] = [5];
// x=5, y=20 (undefined triggers default, null does NOT)

// Swap without temp variable
let p = 1, q = 2;
[p, q] = [q, p];
// p=2, q=1

// From any iterable (generators, strings, Sets, Maps)
const [char1, char2] = "hello";
// char1="h", char2="e"

const [key, value] = new Map([["a", 1]]).entries().next().value;

// Nested destructuring
const [[a1, a2], [b1, b2]] = [[1, 2], [3, 4]];

// Ignore rest if not needed
const [,, onlyThird] = [1, 2, 3];

Object Destructuring

const { name, age } = { name: "Alice", age: 30, city: "NYC" };
// name="Alice", age=30  (city ignored)

// Rename while destructuring
const { name: userName, age: userAge } = { name: "Alice", age: 30 };
// userName="Alice", userAge=30

// Default values
const { role = "user", name: n = "Anonymous" } = { name: "Alice" };
// role="user", n="Alice"

// Nested object destructuring
const { address: { street, city } } = {
  address: { street: "123 Main", city: "Springfield" }
};

// Rest in object destructuring (ES2018)
const { a: ignored, ...rest } = { a: 1, b: 2, c: 3 };
// rest = { b: 2, c: 3 }

// Function parameter destructuring
function render({ title, body = "", maxWidth = 800 } = {}) {
  return `

${title}

${body}

`; } // Combined array + object const { results: [first, second] } = { results: [{ id: 1 }, { id: 2 }] }; // Aliased + nested + default const { user: { profile: { avatar: avatarUrl = "/default.png" } = {} } = {} } = apiResponse ?? {};

Spread Operator

// Spread arrays
const merged = [...arr1, ...arr2];
const copy = [...original];  // shallow copy
const withNew = [...arr, newItem];
const sorted = [...arr].sort();  // sort without mutating original

// Spread objects (ES2018)
const merged = { ...defaults, ...overrides };   // rightmost wins
const clone = { ...original };                  // shallow copy

// Shallow copy caveats — nested objects are still references
const original = { user: { name: "Alice" } };
const copy = { ...original };
copy.user.name = "Bob";
original.user.name;  // "Bob" — shared reference!

// Use structuredClone for deep clone (ES2022)
const deep = structuredClone(original);
deep.user.name = "Bob";
original.user.name;  // "Alice" — independent copy

// Spread in function calls
function add3(a, b, c) { return a + b + c; }
const args = [1, 2, 3];
add3(...args);  // 6

// Convert array-like to real array
const nodeList = document.querySelectorAll("div");
const arr = [...nodeList];  // real array with map, filter, etc.

6. Iterators & Generators

The Iterator Protocol

An object is an iterator if it has a next() method returning { value, done }. An object is iterable if it has [Symbol.iterator]() returning an iterator.

// Manual iterator
function makeRangeIterator(start, end) {
  let current = start;
  return {
    next() {
      if (current <= end) {
        return { value: current++, done: false };
      }
      return { value: undefined, done: true };
    }
  };
}

// Making an object iterable
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  // [Symbol.iterator] makes this class iterable
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      },
      [Symbol.iterator]() { return this; }  // iterators should be iterable
    };
  }
}

const range = new Range(1, 5);
[...range];                    // [1, 2, 3, 4, 5]
for (const n of range) { }    // works
const [first] = range;         // 1 — destructuring uses iterator protocol

Generators as Iterators

// Generator function automatically implements iterator protocol
class InfiniteSequence {
  constructor(start = 0, step = 1) {
    this.start = start;
    this.step = step;
  }

  *[Symbol.iterator]() {  // generator method
    let n = this.start;
    while (true) {
      yield n;
      n += this.step;
    }
  }
}

const evens = new InfiniteSequence(0, 2);
const first5 = [];
for (const n of evens) {
  first5.push(n);
  if (first5.length === 5) break;  // safe to break from infinite iterators
}
// first5 = [0, 2, 4, 6, 8]

// yield* for composition
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* flatten(item);  // recursive delegation
    } else {
      yield item;
    }
  }
}
[...flatten([1, [2, [3, [4]]], 5])];  // [1, 2, 3, 4, 5]

// Lazy evaluation — only computes what's consumed
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(n, iterable) {
  let count = 0;
  for (const item of iterable) {
    if (count++ >= n) break;
    yield item;
  }
}

function* filter(pred, iterable) {
  for (const item of iterable) {
    if (pred(item)) yield item;
  }
}

// Pipeline — no intermediate arrays created
const result = [...take(5, filter(n => n % 2 === 0, naturals()))];
// [2, 4, 6, 8, 10]

Async Iterators

// Async iterator protocol: next() returns Promise<{ value, done }>
class AsyncRange {
  constructor(start, end, delay = 100) {
    this.start = start;
    this.end = end;
    this.delay = delay;
  }

  [Symbol.asyncIterator]() {
    let current = this.start;
    const { end, delay } = this;
    return {
      next() {
        return new Promise(resolve => {
          setTimeout(() => {
            if (current <= end) {
              resolve({ value: current++, done: false });
            } else {
              resolve({ value: undefined, done: true });
            }
          }, delay);
        });
      }
    };
  }
}

async function main() {
  for await (const n of new AsyncRange(1, 5)) {
    console.log(n);  // 1, 2, 3, 4, 5 — each delayed 100ms
  }
}

// Async generator — the clean way to create async iterators
async function* pollApi(url, interval) {
  while (true) {
    const data = await fetch(url).then(r => r.json());
    yield data;
    await new Promise(r => setTimeout(r, interval));
  }
}

// Works with for await...of
for await (const data of pollApi("/api/status", 5000)) {
  updateUI(data);
  if (data.done) break;
}

7. Promises & Async

Promise States and Constructor

// A Promise is in one of three states: pending, fulfilled, rejected
// State transitions are one-way and irreversible

const p = new Promise((resolve, reject) => {
  // Executor runs synchronously
  const success = true;
  if (success) {
    resolve("value");   // transitions to fulfilled
  } else {
    reject(new Error("something failed"));  // transitions to rejected
  }
});

// Promise.resolve / Promise.reject — create already-settled promises
const fulfilled = Promise.resolve(42);
const rejected = Promise.reject(new Error("oops"));

// Wrapping callback-based APIs
function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// util.promisify does this automatically (Node.js)
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

Chaining: then / catch / finally

fetch('/api/user')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();   // returns another promise — chain continues
  })
  .then(user => {
    console.log(user.name);
    return user.id;           // non-promise values are auto-wrapped
  })
  .catch(err => {
    // catches rejections from ANY prior .then()
    console.error("Failed:", err.message);
    return null;              // returning a value recovers the chain
    // re-throw if you can't recover: throw err;
  })
  .finally(() => {
    // runs regardless of success or failure — good for cleanup
    hideLoadingSpinner();
    // NOTE: finally does not receive a value, and its return is ignored
    // UNLESS it returns a rejected promise
  });

// .then(onFulfilled, onRejected) — second arg handles local rejection only
p.then(
  value => console.log("fulfilled:", value),
  err => console.error("rejected:", err.message)
);

Promise Combinators

MethodResolves whenRejects whenUse case
Promise.all()All fulfillAny rejects (fail-fast)Parallel, all required
Promise.allSettled()All settle (any state)NeverParallel, partial failure OK
Promise.race()First to settleFirst to settle (if reject)Timeouts, first wins
Promise.any()First to fulfillAll reject (AggregateError)Try multiple sources, first OK
// Promise.all — parallel fetch, fails fast
const [user, posts, comments] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json()),
]);

// Promise.allSettled — handle each result individually
const results = await Promise.allSettled([
  fetch('/api/primary'),
  fetch('/api/fallback'),
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log("Success:", result.value);
  } else {
    console.error("Failed:", result.reason);
  }
});

// Promise.race — timeout pattern
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), ms)
  );
  return Promise.race([promise, timeout]);
}

// Promise.any — try multiple CDNs
const fastest = await Promise.any([
  fetch("https://cdn1.example.com/lib.js"),
  fetch("https://cdn2.example.com/lib.js"),
  fetch("https://cdn3.example.com/lib.js"),
]);

async / await

// async function always returns a Promise
async function fetchUser(id) {
  // await unwraps a Promise — suspends function, not the thread
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new Error(`Failed: ${response.status}`);  // becomes a rejected promise
  }

  return response.json();  // return value becomes the resolved value
}

// Error handling with try/catch — equivalent to .catch()
async function safeLoad(id) {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (err) {
    if (err.message.includes("404")) {
      return null;  // graceful recovery
    }
    throw err;  // re-throw unexpected errors
  } finally {
    cleanup();  // always runs
  }
}

// Sequential — waits for each before starting the next
async function sequential(ids) {
  const results = [];
  for (const id of ids) {
    results.push(await fetchUser(id));  // 3 round trips sequentially
  }
  return results;
}

// Parallel — start all, then await all
async function parallel(ids) {
  const promises = ids.map(id => fetchUser(id));  // start all immediately
  return Promise.all(promises);                    // wait for all
}

// Top-level await (ES2022, requires ESM or supported environments)
const config = await fetch('/config.json').then(r => r.json());
Common async/await mistake: await in non-async forEach
Array.forEach ignores returned promises — use for...of for sequential async, or Promise.all(arr.map(...)) for parallel async.
// WRONG — forEach does not await, loop body runs concurrently and errors are lost
items.forEach(async (item) => {
  await process(item);  // Not actually awaited by forEach
});

// CORRECT (sequential)
for (const item of items) {
  await process(item);
}

// CORRECT (parallel)
await Promise.all(items.map(item => process(item)));

// CORRECT (parallel with concurrency limit using a semaphore)
async function mapWithLimit(items, limit, fn) {
  const results = [];
  const executing = [];
  for (const item of items) {
    const p = fn(item).then(r => { results.push(r); });
    executing.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(e => e === p), 1);
    }
  }
  await Promise.all(executing);
  return results;
}

8. Modules

ES Modules (ESM)

// --- math.js ---
// Named exports
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Vector { /* ... */ }

// Default export — one per module
export default class Calculator { /* ... */ }

// Re-exports — expose from another module
export { add, PI } from './math-utils.js';
export { default as Vector } from './vector.js';
export * from './helpers.js';          // re-export all named
export * as utils from './helpers.js'; // re-export as namespace

// --- app.js ---
import Calculator from './math.js';           // default import
import { PI, add } from './math.js';          // named imports
import { add as sum } from './math.js';       // renamed
import * as math from './math.js';            // namespace import
import Calculator, { PI, add } from './math.js'; // mixed

// import.meta — module metadata (ESM only)
import.meta.url;   // file URL of this module
import.meta.env;   // Vite/bundler inject env vars here

Dynamic import()

// Load a module lazily at runtime — returns a Promise
async function loadFeature(featureName) {
  try {
    const module = await import(`./features/${featureName}.js`);
    return module.default;
  } catch (err) {
    console.error(`Failed to load feature: ${featureName}`, err);
    return null;
  }
}

// Useful for code splitting — bundlers split at import() boundaries
button.addEventListener('click', async () => {
  const { openModal } = await import('./modal.js');
  openModal();
});

// Conditional loading
if (process.env.NODE_ENV === 'development') {
  const { devtools } = await import('./devtools.js');
  devtools.install();
}

CommonJS vs ESM

// CommonJS (Node.js legacy)
const fs = require('fs');
const { join } = require('path');
module.exports = { myFunction };
module.exports.default = MyClass;

// ESM (modern, preferred)
import fs from 'fs';
import { join } from 'path';
export { myFunction };
export default MyClass;
FeatureCommonJS (CJS)ES Modules (ESM)
LoadingSynchronous, at runtimeAsynchronous, static analysis at parse time
ExportsSingle object, mutableLive bindings, read-only from importer
Circular depsGets partially initialized moduleLive bindings resolve at runtime
Tree shakingNo — whole module is requiredYes — bundlers eliminate unused exports
Top-level awaitNoYes (ES2022)
Browser nativeNo (needs bundler)Yes (type="module")
__dirnameAvailableUse import.meta.url
File extension in Node.js or .cjs.mjs or .js with "type":"module"
ESM live bindings
Unlike CJS where you get a copy of the value, ESM exports are live bindings. If the exporting module changes a value, all importers see the update. This enables circular dependency resolution and is why you cannot reassign an imported name.

9. Collections

Map

// Map — ordered key-value pairs, any key type
const map = new Map();
map.set("key", "value");
map.set(42, "number key");
map.set({}, "object key");     // any reference can be a key

// Create from entries
const map2 = new Map([
  ["name", "Alice"],
  ["age", 30],
]);

map2.get("name");    // "Alice"
map2.has("age");     // true
map2.size;           // 2
map2.delete("age");
map2.clear();

// Iteration — in insertion order
for (const [key, value] of map) { }
[...map.keys()];
[...map.values()];
[...map.entries()];
map.forEach((value, key) => { });

// Convert to/from object
const obj = Object.fromEntries(map);
const mapFromObj = new Map(Object.entries(obj));

Map vs Plain Object

FeatureObjectMap
Key typesString or Symbol onlyAny value (object, function, primitive)
Default keysPrototype keys (toString, etc.)No inherited keys
OrderMostly insertion order (integers first)Strict insertion order
SizeManual (Object.keys(o).length)map.size
IterationNeed Object.entries()Directly iterable
PerformanceGood for small, static dataBetter for frequent add/delete
JSONJSON.stringify worksNeeds manual conversion

Set

// Set — unique values, insertion order preserved
const set = new Set([1, 2, 3, 2, 1]);
set;     // Set(3) {1, 2, 3}

set.add(4);
set.has(3);    // true
set.delete(2);
set.size;      // 3

// Deduplicate an array
const unique = [...new Set([1, 2, 2, 3, 3])];  // [1, 2, 3]

// Set operations (ES2025 native methods)
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);

a.union(b);          // Set {1, 2, 3, 4, 5, 6}
a.intersection(b);   // Set {3, 4}
a.difference(b);     // Set {1, 2}
a.symmetricDifference(b); // Set {1, 2, 5, 6}
a.isSubsetOf(b);     // false
a.isSupersetOf(b);   // false
a.isDisjointFrom(b); // false

// Manual set operations (pre-ES2025)
const union = new Set([...a, ...b]);
const intersection = new Set([...a].filter(x => b.has(x)));
const difference = new Set([...a].filter(x => !b.has(x)));

WeakMap and WeakSet

// WeakMap — object keys only, doesn't prevent GC
const cache = new WeakMap();

function processUser(user) {
  if (cache.has(user)) return cache.get(user);
  const result = heavyComputation(user);
  cache.set(user, result);
  return result;
}
// When user object is garbage collected, cache entry is automatically removed

// WeakSet — object members only, doesn't prevent GC
const seen = new WeakSet();

function processOnce(obj) {
  if (seen.has(obj)) return;
  seen.add(obj);
  doWork(obj);
}

// Key differences from Map/Set:
// - Keys/members must be objects (or registered symbols)
// - Not iterable (no .forEach, no spread, no for...of)
// - No .size property
// - Entries can be collected by GC at any time

WeakRef and FinalizationRegistry

// WeakRef — reference that doesn't prevent GC
let obj = { data: "expensive" };
const ref = new WeakRef(obj);

// Later:
const deref = ref.deref();
if (deref !== undefined) {
  console.log(deref.data);  // still alive
} else {
  console.log("Object was collected");
}

// FinalizationRegistry — callback when object is GC'd
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object with id ${heldValue} was collected`);
});

let resource = { id: 42 };
registry.register(resource, resource.id);  // register with "held value"

// When resource is GC'd, callback fires with heldValue=42
resource = null;  // allow GC
WeakRef / FinalizationRegistry caveats
GC timing is not guaranteed or predictable. Do not use these for correctness-critical logic — only for optimization (caches, cleanup). The callback may never fire in short-lived programs.

10. Proxy & Reflect

Proxy Fundamentals

A Proxy wraps a target object and intercepts fundamental operations via handler traps.

const target = { name: "Alice", age: 30 };

const handler = {
  // get trap — intercepts property reads
  get(target, prop, receiver) {
    console.log(`Getting ${prop}`);
    return Reflect.get(target, prop, receiver);  // use Reflect for correct behavior
  },

  // set trap — intercepts property writes
  set(target, prop, value, receiver) {
    console.log(`Setting ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },

  // has trap — intercepts `in` operator
  has(target, prop) {
    return prop in target;
  },

  // deleteProperty trap — intercepts `delete`
  deleteProperty(target, prop) {
    console.log(`Deleting ${prop}`);
    return Reflect.deleteProperty(target, prop);
  },

  // apply trap — intercepts function calls (target must be a function)
  apply(target, thisArg, argumentsList) {
    console.log(`Called with args: ${argumentsList}`);
    return Reflect.apply(target, thisArg, argumentsList);
  },

  // construct trap — intercepts `new`
  construct(target, argumentsList, newTarget) {
    return Reflect.construct(target, argumentsList, newTarget);
  },
};

const proxy = new Proxy(target, handler);
proxy.name;       // logs "Getting name", returns "Alice"
proxy.age = 31;   // logs "Setting age = 31"

Reflect API

Why use Reflect inside proxy handlers?
Reflect provides the default behavior for each trap. Always use Reflect.method() instead of operating on the target directly — it correctly handles edge cases like inheritance and receiver binding.
// Reflect methods mirror proxy traps
Reflect.get(obj, 'prop', receiver)          // obj.prop
Reflect.set(obj, 'prop', value, receiver)   // obj.prop = value; returns boolean
Reflect.has(obj, 'prop')                    // 'prop' in obj
Reflect.deleteProperty(obj, 'prop')         // delete obj.prop; returns boolean
Reflect.apply(fn, thisArg, args)            // fn.apply(thisArg, args)
Reflect.construct(Class, args, newTarget)   // new Class(...args)
Reflect.defineProperty(obj, 'prop', desc)  // Object.defineProperty
Reflect.getOwnPropertyDescriptor(obj, 'p') // Object.getOwnPropertyDescriptor
Reflect.ownKeys(obj)                        // all own keys (string + symbol)
Reflect.getPrototypeOf(obj)                 // Object.getPrototypeOf
Reflect.setPrototypeOf(obj, proto)          // Object.setPrototypeOf
Reflect.isExtensible(obj)                   // Object.isExtensible
Reflect.preventExtensions(obj)              // Object.preventExtensions

Practical Patterns

Validation Proxy

function createValidator(target, schema) {
  return new Proxy(target, {
    set(obj, prop, value) {
      if (prop in schema) {
        const { type, min, max } = schema[prop];
        if (typeof value !== type) {
          throw new TypeError(`${prop} must be ${type}, got ${typeof value}`);
        }
        if (min !== undefined && value < min) {
          throw new RangeError(`${prop} must be >= ${min}`);
        }
        if (max !== undefined && value > max) {
          throw new RangeError(`${prop} must be <= ${max}`);
        }
      }
      return Reflect.set(obj, prop, value);
    }
  });
}

const user = createValidator({}, {
  age: { type: 'number', min: 0, max: 150 },
  name: { type: 'string' },
});

user.name = "Alice";  // OK
user.age = 200;       // RangeError: age must be <= 150

Reactive Data (Vue-like)

function reactive(target, onChange) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const oldValue = obj[prop];
      const result = Reflect.set(obj, prop, value);
      if (oldValue !== value) {
        onChange(prop, value, oldValue);
      }
      return result;
    },
    get(obj, prop) {
      const value = Reflect.get(obj, prop);
      // Recursively wrap nested objects
      if (typeof value === 'object' && value !== null) {
        return reactive(value, onChange);
      }
      return value;
    }
  });
}

const state = reactive({ count: 0, user: { name: "Alice" } }, (prop, next, prev) => {
  console.log(`${prop}: ${prev} → ${next}`);
  renderUI();
});

state.count++;          // logs "count: 0 → 1"
state.user.name = "Bob"; // logs "name: Alice → Bob"

11. Error Handling

Built-in Error Types

Error TypeWhen thrown
ErrorBase type — generic errors
TypeErrorWrong type (null access, wrong arg type, calling non-function)
RangeErrorValue out of valid range (array length, stack overflow)
ReferenceErrorAccessing undeclared variable
SyntaxErrorInvalid syntax (thrown by parser, not catchable in same script)
URIErrorMalformed URI (decodeURIComponent("%"))
EvalErrorRarely thrown in modern JS
AggregateErrorMultiple errors (e.g., Promise.any all reject)

try / catch / finally

try {
  const data = JSON.parse(invalidJson);   // throws SyntaxError
  riskyOperation(data);
} catch (err) {
  // err is the thrown value (convention: Error instance, but can be anything)
  if (err instanceof SyntaxError) {
    console.error("Invalid JSON:", err.message);
  } else if (err instanceof NetworkError) {
    console.error("Network failure:", err.message);
  } else {
    throw err;  // re-throw unknown errors — don't swallow them
  }
} finally {
  // Runs always — even after return/throw in try or catch
  releaseResources();
}

// Omit the binding if you don't need it (ES2019)
try {
  doSomething();
} catch {
  // no binding needed
  console.error("Something failed");
}

Custom Errors

// Extend Error for domain-specific error types
class AppError extends Error {
  constructor(message, options = {}) {
    super(message, { cause: options.cause });  // pass cause to Error (ES2022)
    this.name = this.constructor.name;         // "AppError", not "Error"
    this.code = options.code;
    this.statusCode = options.statusCode;

    // Preserve stack trace (V8-specific)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}

class ValidationError extends AppError {
  constructor(field, message) {
    super(`Validation failed for ${field}: ${message}`, { code: "VALIDATION_ERROR", statusCode: 400 });
    this.field = field;
  }
}

class NotFoundError extends AppError {
  constructor(resource, id) {
    super(`${resource} with id ${id} not found`, { code: "NOT_FOUND", statusCode: 404 });
    this.resource = resource;
  }
}

// Error cause — chain errors without losing context (ES2022)
async function loadUser(id) {
  try {
    return await db.query('SELECT * FROM users WHERE id = $1', [id]);
  } catch (err) {
    throw new AppError("Failed to load user", {
      cause: err,  // original error preserved
      code: "DB_ERROR",
    });
  }
}

// Access the cause
try {
  await loadUser(1);
} catch (err) {
  console.error(err.message);          // "Failed to load user"
  console.error(err.cause.message);    // original DB error
  console.error(err.cause?.code);      // original error's code if any
}

AggregateError

// AggregateError — wraps multiple errors (used by Promise.any)
try {
  await Promise.any([
    Promise.reject(new Error("DNS failure")),
    Promise.reject(new Error("Timeout")),
    Promise.reject(new Error("Rate limited")),
  ]);
} catch (err) {
  if (err instanceof AggregateError) {
    console.error(`All ${err.errors.length} attempts failed:`);
    err.errors.forEach(e => console.error(" -", e.message));
  }
}

// Create manually
throw new AggregateError(
  [new Error("err1"), new Error("err2")],
  "Multiple operations failed"
);

Async Error Handling Patterns

// Pattern 1: try/catch with async/await (preferred)
async function run() {
  try {
    const result = await fetchData();
    return result;
  } catch (err) {
    handleError(err);
  }
}

// Pattern 2: Result type (no exceptions in business logic)
async function safeRun() {
  try {
    const value = await fetchData();
    return { ok: true, value };
  } catch (error) {
    return { ok: false, error };
  }
}

const { ok, value, error } = await safeRun();
if (!ok) { /* handle */ }

// Pattern 3: Unhandled rejection global handler
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection:', reason);
  process.exit(1);
});

// Browser equivalent
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled rejection:', event.reason);
  event.preventDefault();  // prevent default console error
});

12. Modern Features

Optional Chaining & Nullish Coalescing

// Optional chaining (?.) — short-circuits on null/undefined
const street = user?.address?.street;       // undefined if any step is null/undefined
const firstItem = arr?.[0];                 // safe array access
const result = obj?.method?.();             // safe method call

// Practical: deep optional access
const avatarUrl = user?.profile?.avatar?.url ?? "/default-avatar.png";

// Nullish coalescing (??) — only null/undefined triggers fallback
const name = user.name ?? "Anonymous";      // "" is a valid name (not replaced)
const count = response.count ?? 0;          // 0 in response is valid

// ?? vs || difference — critical distinction
0 || "default"    // "default"  (0 is falsy)
0 ?? "default"    // 0          (0 is not null/undefined)

"" || "default"   // "default"  ("" is falsy)
"" ?? "default"   // ""         ("" is not null/undefined)

Logical Assignment Operators

// ||= logical OR assignment — assign if left is falsy
let a = null;
a ||= "default";   // a = "default"

let b = 0;
b ||= 42;          // b = 42  (0 is falsy)

// &&= logical AND assignment — assign if left is truthy
let c = { name: "Alice" };
c &&= processUser(c);  // only calls processUser if c is truthy

let d = null;
d &&= processUser(d);  // d remains null, processUser never called

// ??= nullish assignment — assign only if left is null/undefined
let config = null;
config ??= loadDefaultConfig();  // config = loadDefaultConfig()

let count = 0;
count ??= 100;     // count stays 0 (0 is not null/undefined)

// Practical: initialize object property only if absent
options.timeout ??= 5000;
options.retries ??= 3;

structuredClone

// Deep clone built into the platform (ES2022)
const original = {
  name: "Alice",
  scores: [1, 2, 3],
  date: new Date(),     // Date is cloned properly
  nested: { x: 1 },
  map: new Map([["a", 1]]),   // Map/Set supported
  set: new Set([1, 2, 3]),
};

const clone = structuredClone(original);
clone.nested.x = 99;
original.nested.x;  // still 1

// Limitations — these throw:
// - Functions
// - DOM nodes
// - Class instances that use symbols (class fields work, prototype methods don't transfer)
// - Error objects (cause not preserved)

// Transfer ownership (move, not copy — for large buffers)
const buffer = new ArrayBuffer(1024);
const moved = structuredClone(buffer, { transfer: [buffer] });
// buffer is now detached (zero-length)

Array.at() and Object.groupBy()

// Array.at() — indexing from the end (ES2022)
const arr = [1, 2, 3, 4, 5];
arr.at(0);   // 1
arr.at(-1);  // 5  (last)
arr.at(-2);  // 4  (second to last)

// String.at() works the same
"hello".at(-1);  // "o"

// Object.groupBy() — group iterable by a key function (ES2024)
const people = [
  { name: "Alice", role: "admin" },
  { name: "Bob", role: "user" },
  { name: "Carol", role: "admin" },
];

const byRole = Object.groupBy(people, person => person.role);
// {
//   admin: [{ name: "Alice", role: "admin" }, { name: "Carol", role: "admin" }],
//   user:  [{ name: "Bob", role: "user" }]
// }

// Map.groupBy() — when you need non-string keys
const byLength = Map.groupBy(["one", "two", "three"], word => word.length);
// Map { 3 => ["one", "two"], 5 => ["three"] }

Set Methods (ES2025)

const odds = new Set([1, 3, 5, 7, 9]);
const primes = new Set([2, 3, 5, 7]);

odds.intersection(primes);          // Set {3, 5, 7}
odds.union(primes);                 // Set {1, 2, 3, 5, 7, 9}
odds.difference(primes);            // Set {1, 9}
odds.symmetricDifference(primes);   // Set {1, 2, 9}
odds.isSubsetOf(primes);           // false
primes.isSupersetOf(new Set([3])); // true
odds.isDisjointFrom(primes);       // false

using Declarations (ES2025)

// Explicit resource management — like RAII in C++
// Objects with [Symbol.dispose]() are automatically cleaned up

class DatabaseConnection {
  constructor(url) {
    this.conn = connect(url);
  }

  [Symbol.dispose]() {
    this.conn.close();
    console.log("Connection closed");
  }
}

// using — synchronous disposal at end of block
{
  using db = new DatabaseConnection(DB_URL);
  await db.query("SELECT 1");
  // db[Symbol.dispose]() called automatically here
}
// Connection is always closed — even if an exception is thrown

// await using — for async disposal
class FileHandle {
  async [Symbol.asyncDispose]() {
    await this.handle.close();
  }
}

{
  await using fh = await openFile("data.txt");
  const content = await fh.read();
  // fh[Symbol.asyncDispose]() awaited automatically
}

13. Event Loop

Execution Model

JavaScript is single-threaded. The event loop coordinates:

  • Call stack — current synchronous execution (LIFO)
  • Microtask queue — high-priority async callbacks (Promises, queueMicrotask)
  • Macrotask queue — lower-priority callbacks (setTimeout, setInterval, I/O, UI events)
Event loop tick
After each macrotask completes, the engine drains all microtasks before picking the next macrotask. This means microtasks can starve the event loop if they generate more microtasks indefinitely.
console.log("1 — sync");

setTimeout(() => console.log("2 — macrotask"), 0);

Promise.resolve()
  .then(() => console.log("3 — microtask 1"))
  .then(() => console.log("4 — microtask 2"));

queueMicrotask(() => console.log("5 — microtask 3"));

console.log("6 — sync");

// Output order:
// 1 — sync
// 6 — sync
// 3 — microtask 1   ← microtasks run after sync, before macrotasks
// 5 — microtask 3
// 4 — microtask 2   ← chained .then() enqueues after microtask 1 runs
// 2 — macrotask     ← runs after all microtasks are drained
// Async/await desugars to Promise chains — same microtask behavior
async function main() {
  console.log("A");
  await Promise.resolve();   // suspends here, re-enters as microtask
  console.log("B");          // runs in microtask queue
}

console.log("start");
main();
console.log("end");

// Output: start → A → end → B

Macrotask vs Microtask Sources

QueueSources
MicrotaskPromise callbacks (.then/.catch/.finally), queueMicrotask(), MutationObserver, await resumption
MacrotasksetTimeout, setInterval, setImmediate (Node), I/O callbacks, UI events, MessageChannel
Animation framerequestAnimationFrame — before next repaint, after microtasks
// MessageChannel — create a macrotask (useful for yielding to browser)
function yieldToMain() {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel();
    port1.onmessage = resolve;
    port2.postMessage(null);
  });
}

// Long task chunking — prevents UI jank
async function processLargeArray(items) {
  const CHUNK_SIZE = 100;
  const results = [];

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    results.push(...chunk.map(process));

    // Yield to allow rendering, input handling between chunks
    await yieldToMain();
  }

  return results;
}

// Scheduler API (Chrome) — more sophisticated yielding
async function processWithScheduler(items) {
  for (const item of items) {
    if (navigator.scheduling?.isInputPending()) {
      await scheduler.yield();  // yield to user input
    }
    processItem(item);
  }
}

Common Event Loop Gotchas

// setTimeout(fn, 0) is NOT immediate — goes to macrotask queue
// Microtasks ALWAYS run before the next setTimeout

// Microtask infinite loop — starvation!
function infiniteMicrotasks() {
  Promise.resolve().then(infiniteMicrotasks);  // never reaches macrotasks!
}

// setTimeout minimum delay — browsers clamp to ~4ms after 5 nested levels
// Not suitable for high-precision timing

// async function is NOT blocking — it returns immediately
async function slowOp() {
  await sleep(1000);
  return "done";
}
slowOp();                // returns Promise immediately
// "done" is NOT available synchronously

14. DOM & Browser APIs

DOM Querying and Manipulation

// Modern selectors
const el = document.querySelector(".my-class");        // first match
const all = document.querySelectorAll("div.card");      // NodeList
const allArr = [...document.querySelectorAll("li")];   // real Array

// Traversal
el.parentElement;
el.children;              // HTMLCollection of direct children
el.firstElementChild;
el.nextElementSibling;
el.closest(".container"); // walk up tree to find matching ancestor

// Creation and insertion
const div = document.createElement("div");
div.textContent = "Hello";            // safe (no XSS)
div.innerHTML = "Hi"; // dangerous with user input!
div.className = "card active";
div.setAttribute("data-id", "42");
div.classList.add("active");
div.classList.remove("active");
div.classList.toggle("active");
div.classList.contains("active");

parent.appendChild(div);
parent.prepend(div);
parent.insertBefore(div, referenceNode);
el.remove();

// insertAdjacentHTML — avoids full re-parse
el.insertAdjacentHTML("beforeend", "
  • New item
  • "); // safer than innerHTML // DocumentFragment — batch DOM updates (single reflow) const frag = document.createDocumentFragment(); items.forEach(item => { const li = document.createElement("li"); li.textContent = item.name; frag.appendChild(li); }); ul.appendChild(frag); // single DOM operation

    Event Delegation

    // Instead of adding listeners to every child, add one to the parent
    // Handles dynamically added elements, fewer listeners = better memory
    
    document.getElementById("todo-list").addEventListener("click", (event) => {
      // event.target is the actual clicked element
      const item = event.target.closest("li.todo-item");
      if (!item) return;  // click was not on a todo item
    
      if (event.target.matches(".delete-btn")) {
        item.remove();
      } else if (event.target.matches(".complete-btn")) {
        item.classList.toggle("completed");
      }
    });
    
    // Event object properties
    event.target;           // element that was actually clicked
    event.currentTarget;    // element the listener is attached to
    event.bubbles;          // whether event bubbles
    event.stopPropagation(); // stop bubbling
    event.preventDefault();  // prevent default browser action (form submit, link follow)
    event.stopImmediatePropagation(); // stop other listeners on same element too

    Fetch API

    // Basic fetch
    const response = await fetch("/api/users");
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const users = await response.json();
    
    // POST with JSON body
    const response = await fetch("/api/users", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`,
      },
      body: JSON.stringify({ name: "Alice" }),
    });
    
    // File upload
    const formData = new FormData();
    formData.append("file", fileInput.files[0]);
    formData.append("name", "My File");
    const response = await fetch("/upload", { method: "POST", body: formData });
    
    // AbortController — cancel requests
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);
    
    try {
      const response = await fetch("/api/slow-endpoint", {
        signal: controller.signal,
      });
      clearTimeout(timeoutId);
    } catch (err) {
      if (err.name === "AbortError") {
        console.log("Request was cancelled");
      }
    }

    IntersectionObserver

    // Observe when elements enter/exit the viewport
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            // Element entered viewport
            entry.target.classList.add("visible");
            // For lazy loading — stop observing after load
            observer.unobserve(entry.target);
          }
        });
      },
      {
        root: null,         // null = viewport
        rootMargin: "0px",  // margin around root
        threshold: 0.1,     // 10% visible triggers callback
      }
    );
    
    // Lazy load images
    document.querySelectorAll("img[data-src]").forEach(img => {
      observer.observe(img);
    });
    
    // Infinite scroll
    const sentinel = document.getElementById("load-more-sentinel");
    const scrollObserver = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) loadMoreItems();
    });
    scrollObserver.observe(sentinel);

    Web Workers

    // Main thread — offload CPU-intensive work
    const worker = new Worker("./heavy-worker.js");
    
    worker.postMessage({ data: largeArray });  // structured clone
    
    worker.onmessage = (event) => {
      console.log("Worker result:", event.data);
    };
    
    worker.onerror = (err) => {
      console.error("Worker error:", err.message);
    };
    
    // Clean up
    worker.terminate();
    
    // --- heavy-worker.js ---
    self.onmessage = (event) => {
      const { data } = event.data;
      const result = expensiveComputation(data);  // runs off main thread
      self.postMessage(result);
    };
    
    // SharedArrayBuffer + Atomics — shared memory between workers (advanced)
    const sharedBuffer = new SharedArrayBuffer(1024);
    const sharedArray = new Int32Array(sharedBuffer);
    worker.postMessage({ buffer: sharedBuffer });
    
    // In worker:
    Atomics.add(new Int32Array(event.data.buffer), 0, 1);  // atomic increment

    Storage API

    // localStorage — persists across sessions, ~5MB, synchronous
    localStorage.setItem("key", JSON.stringify(value));
    const val = JSON.parse(localStorage.getItem("key") ?? "null");
    localStorage.removeItem("key");
    localStorage.clear();
    
    // sessionStorage — cleared when tab closes, same API
    sessionStorage.setItem("token", authToken);
    
    // IndexedDB — for larger structured data (async, transactional)
    const request = indexedDB.open("mydb", 1);
    request.onupgradeneeded = (e) => {
      const db = e.target.result;
      db.createObjectStore("users", { keyPath: "id" });
    };
    request.onsuccess = (e) => {
      const db = e.target.result;
      const tx = db.transaction("users", "readwrite");
      tx.objectStore("users").add({ id: 1, name: "Alice" });
    };
    
    // Cache API — for service workers and offline support
    const cache = await caches.open("v1");
    await cache.add("/offline.html");
    const response = await cache.match("/offline.html");

    15. Common Pitfalls

    The 4 Rules of `this`

    How `this` is determined — in priority order
    1. new bindingnew Fn(): this is the newly created object
    2. Explicit bindingfn.call(ctx) / fn.apply(ctx) / fn.bind(ctx)(): this is ctx
    3. Implicit bindingobj.fn(): this is obj
    4. Default bindingfn(): this is undefined (strict) or window (sloppy)
    Arrow functions are exempt — they inherit this lexically and cannot be overridden.
    function show() { console.log(this); }
    
    // Default
    show();                      // undefined (strict mode) or window
    
    // Implicit
    const obj = { show };
    obj.show();                  // obj
    
    // Explicit
    show.call({ x: 1 });        // { x: 1 }
    show.apply({ x: 2 });       // { x: 2 }
    const bound = show.bind({ x: 3 });
    bound();                     // { x: 3 }
    
    // new
    function Foo() { this.x = 1; }
    const f = new Foo();         // this = new empty object, then returned
    
    // Lost binding — common bug
    const { show: detached } = obj;
    detached();                  // undefined (this is lost)
    
    // Fix with bind
    const detachedFixed = obj.show.bind(obj);
    detachedFixed();             // obj

    Closures in Loops (var vs let)

    // BUG: var is function-scoped — all callbacks share same i reference
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0);  // prints 3, 3, 3
    }
    
    // FIX 1: let creates a new binding per iteration
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0);  // prints 0, 1, 2
    }
    
    // FIX 2: IIFE captures current value
    for (var i = 0; i < 3; i++) {
      ((captured) => {
        setTimeout(() => console.log(captured), 0);
      })(i);
    }
    
    // FIX 3: use index from an outer binding that doesn't change
    const fns = [0, 1, 2].map(i => () => console.log(i));

    Floating Point Arithmetic

    // IEEE 754 cannot represent all decimals exactly
    0.1 + 0.2 === 0.3           // false!
    0.1 + 0.2                   // 0.30000000000000004
    
    // Fix: epsilon comparison
    Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON  // true (EPSILON = 2.22e-16)
    
    // Fix: scale to integers for financial math
    function addMoney(a, b) {
      // work in cents
      return Math.round(a * 100 + b * 100) / 100;
    }
    
    // Fix: toFixed (but be aware it returns a string)
    (0.1 + 0.2).toFixed(1)     // "0.3" (string)
    parseFloat((0.1 + 0.2).toFixed(10))  // 0.3 (number)
    
    // Fix: use a library for critical financial calculations (decimal.js, big.js)
    
    // Safe integer limits
    Number.MAX_SAFE_INTEGER      // 9007199254740991 (2^53 - 1)
    Number.MIN_SAFE_INTEGER      // -9007199254740991
    Number.isSafeInteger(9007199254740991)  // true
    Number.isSafeInteger(9007199254740992)  // false!
    
    // Use BigInt for large integers
    const big = 9007199254740992n + 1n;  // correct: 9007199254740993n

    typeof null and Array Checks

    // The typeof null bug — do NOT use typeof to check for null
    typeof null === "object"   // true — historic bug
    null === null              // true — use strict equality
    
    // Check for null and undefined together
    value == null              // true if value is null OR undefined (rare valid use of ==)
    
    // Robust type checking
    function getType(val) {
      if (val === null) return "null";
      if (Array.isArray(val)) return "array";
      return typeof val;
    }
    
    // Array.sort() sorts as STRINGS by default!
    [10, 9, 2, 1, 20].sort()           // [1, 10, 2, 20, 9] — lexicographic!
    [10, 9, 2, 1, 20].sort((a, b) => a - b)  // [1, 2, 9, 10, 20] — numeric ascending
    [10, 9, 2, 1, 20].sort((a, b) => b - a)  // [20, 10, 9, 2, 1] — numeric descending
    
    // for...in vs for...of
    const arr = [1, 2, 3];
    arr.custom = "oops";
    
    for (const key in arr) {
      console.log(key);   // "0", "1", "2", "custom" — includes inherited props!
    }
    
    for (const val of arr) {
      console.log(val);   // 1, 2, 3 — values only, no inherited props
    }
    
    // Always use for...of for arrays; for...in only for plain objects
    for (const key in plainObj) {
      if (Object.hasOwn(plainObj, key)) {  // ES2022, or use hasOwnProperty
        console.log(key, plainObj[key]);
      }
    }

    Object Comparison

    // Objects are compared by reference, NOT by value
    const a = { x: 1 };
    const b = { x: 1 };
    const c = a;
    
    a === b;  // false — different objects in memory
    a === c;  // true  — same reference
    
    // Deep equality — no native option, use a library or manual comparison
    function deepEqual(a, b) {
      if (a === b) return true;
      if (typeof a !== typeof b) return false;
      if (typeof a !== "object" || a === null) return false;
    
      const keysA = Object.keys(a);
      const keysB = Object.keys(b);
      if (keysA.length !== keysB.length) return false;
    
      return keysA.every(key => deepEqual(a[key], b[key]));
    }
    
    // Alternatives: JSON.stringify (handles simple cases, order-sensitive, no Date/undefined)
    JSON.stringify(a) === JSON.stringify(b);  // works for simple objects

    Additional Pitfalls Quick Reference

    // parseInt needs a radix!
    parseInt("010")          // 10 in modern JS (was 8 in legacy!) — always specify
    parseInt("010", 10)      // 10 — decimal
    parseInt("010", 8)       // 8  — octal
    parseInt("0x1F", 16)     // 31 — hex (0x prefix handles this)
    
    // parseFloat vs Number
    parseFloat("3.14abc")    // 3.14 — stops at non-numeric
    Number("3.14abc")        // NaN  — stricter
    
    // Arguments object is NOT an array
    function old() {
      arguments.map(fn);     // TypeError! arguments is array-like, not Array
      [...arguments].map(fn); // OK
    }
    // Prefer rest params: function modern(...args) { args.map(fn); }
    
    // delete only removes own enumerable props
    const obj = { x: 1 };
    delete obj.x;            // true, x removed
    delete obj.toString;     // true, but toString is inherited — still accessible
    
    // NaN is not equal to itself — use Number.isNaN()
    NaN === NaN              // false
    Number.isNaN(NaN)        // true
    Number.isNaN("hello")    // false (safe — doesn't coerce like global isNaN)
    isNaN("hello")           // true (dangerous — coerces to NaN first!)
    
    // Labeled statements and break — for nested loops
    outer: for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (i === 1 && j === 1) break outer;  // exits both loops
      }
    }
    
    // String comparison is lexicographic
    "banana" < "cherry"    // true
    "10" < "9"            // true! ("1" < "9" as characters)
    10 < 9                // false (numbers compare correctly)