${title}
${body}
Modern ES6+ language features — quick reference for experienced developers
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
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
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.
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
package-lock.json = npm, yarn.lock = yarn, pnpm-lock.yaml = pnpm).
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.
VS Code has first-class JavaScript and TypeScript support built in. Two extensions are effectively mandatory on any real project:
dbaeumer.vscode-eslint) — surfaces lint errors inline as you type.esbenp.prettier-vscode) — opinionated auto-formatter; eliminates style debates."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.
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.
JavaScript has 7 primitive types plus object (the only structural type):
| Type | typeof result | Example values | Notes |
|---|---|---|---|
undefined | "undefined" | undefined | Uninitialized variable or missing property |
null | "object" | null | Historic bug — null is NOT an object |
boolean | "boolean" | true, false | |
number | "number" | 42, 3.14, NaN, Infinity | 64-bit IEEE 754 float |
bigint | "bigint" | 9007199254740993n | Arbitrary precision integers (ES2020) |
string | "string" | "hello", 'world', `template` | Immutable UTF-16 sequences |
symbol | "symbol" | Symbol("id") | Unique, non-enumerable keys (ES2015) |
object | "object" | {}, [], null | All non-primitive values + null (bug) |
// 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
JavaScript uses three abstract operations for coercion. Understanding them explains most "wat" behavior:
Converts an object to a primitive. Takes an optional hint ("number", "string", or "default"):
[Symbol.toPrimitive], call it with the hint.toString() then valueOf().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]"
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
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]"
=== (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
// 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
// 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
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function / Global | Block | Block |
| Hoisting | Hoisted + initialized to undefined | Hoisted, NOT initialized (TDZ) | Hoisted, NOT initialized (TDZ) |
| Re-declaration | Allowed | Error in same scope | Error in same scope |
| Re-assignment | Allowed | Allowed | Error |
| Global property | Yes (window.x) | No | No |
| Use in loops | Shared across iterations (closure bug) | New binding per iteration | Per iteration (but can't reassign) |
// 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 {}
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 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);
}
// 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
};
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"
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
// 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 parameters — evaluated at call time, not definition time
function createTag(tag = "div", cls = `default-${tag}`) {
return `<${tag} class="${cls}">${tag}>`;
}
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
// 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 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;
}
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 });
// 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
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
// 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]"}'
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];
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 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.
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
// 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 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;
}
// 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);
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)
);
| Method | Resolves when | Rejects when | Use case |
|---|---|---|---|
Promise.all() | All fulfill | Any rejects (fail-fast) | Parallel, all required |
Promise.allSettled() | All settle (any state) | Never | Parallel, partial failure OK |
Promise.race() | First to settle | First to settle (if reject) | Timeouts, first wins |
Promise.any() | First to fulfill | All 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 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());
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;
}
// --- 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
// 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 (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;
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Loading | Synchronous, at runtime | Asynchronous, static analysis at parse time |
| Exports | Single object, mutable | Live bindings, read-only from importer |
| Circular deps | Gets partially initialized module | Live bindings resolve at runtime |
| Tree shaking | No — whole module is required | Yes — bundlers eliminate unused exports |
| Top-level await | No | Yes (ES2022) |
| Browser native | No (needs bundler) | Yes (type="module") |
__dirname | Available | Use import.meta.url |
| File extension in Node | .js or .cjs | .mjs or .js with "type":"module" |
// 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));
| Feature | Object | Map |
|---|---|---|
| Key types | String or Symbol only | Any value (object, function, primitive) |
| Default keys | Prototype keys (toString, etc.) | No inherited keys |
| Order | Mostly insertion order (integers first) | Strict insertion order |
| Size | Manual (Object.keys(o).length) | map.size |
| Iteration | Need Object.entries() | Directly iterable |
| Performance | Good for small, static data | Better for frequent add/delete |
| JSON | JSON.stringify works | Needs manual conversion |
// 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 — 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 — 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
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 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
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
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"
| Error Type | When thrown |
|---|---|
Error | Base type — generic errors |
TypeError | Wrong type (null access, wrong arg type, calling non-function) |
RangeError | Value out of valid range (array length, stack overflow) |
ReferenceError | Accessing undeclared variable |
SyntaxError | Invalid syntax (thrown by parser, not catchable in same script) |
URIError | Malformed URI (decodeURIComponent("%")) |
EvalError | Rarely thrown in modern JS |
AggregateError | Multiple errors (e.g., Promise.any all reject) |
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");
}
// 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 — 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"
);
// 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
});
// 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 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;
// 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() — 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"] }
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
// 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
}
JavaScript is single-threaded. The event loop coordinates:
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
| Queue | Sources |
|---|---|
| Microtask | Promise callbacks (.then/.catch/.finally), queueMicrotask(), MutationObserver, await resumption |
| Macrotask | setTimeout, setInterval, setImmediate (Node), I/O callbacks, UI events, MessageChannel |
| Animation frame | requestAnimationFrame — 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);
}
}
// 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
// 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
// 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
// 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");
}
}
// 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);
// 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
// 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");
new Fn(): this is the newly created objectfn.call(ctx) / fn.apply(ctx) / fn.bind(ctx)(): this is ctxobj.fn(): this is objfn(): this is undefined (strict) or window (sloppy)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
// 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));
// 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
// 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]);
}
}
// 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
// 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)