JavaScript Handbook
JavaScript is a dynamic, prototype-based, single-threaded language created by Brendan Eich in 1995. It is the only language that runs natively in every browser, and with Node.js, Deno, and Bun it also runs on the server. The event loop model makes I/O-heavy workloads (APIs, chat, real-time dashboards) extremely efficient without threads. Modern JS (ES2015+) adds classes, modules, async/await, and destructuring, making it a first-class language for large applications.
Pick JavaScript when
- You're building anything in the browser — JS is the only language that runs natively on the client. There is no alternative without a transpiler.
- You want one language for frontend and backend — Node.js lets the same team share types, validation logic, and models between client and server. Full-stack frameworks (Next.js, Nuxt, SvelteKit) capitalise on this.
- Real-time features — WebSockets, Server-Sent Events, and WebRTC are all JavaScript-native. Building a live chat, collaborative editor, or multiplayer game UI starts here.
- Serverless and edge functions — Cloudflare Workers, Vercel Edge, AWS Lambda@Edge all run JS natively with near-zero cold start. The deployment model fits JS perfectly.
- Large NPM ecosystem — over 2 million packages. UI frameworks (React, Vue, Svelte), bundlers, utilities — the ecosystem is unmatched for web development.
- Rapid UI prototyping — JS + HTML + CSS in a browser gives instant visual feedback. No compile step, no build tool required to get started.
Think twice before choosing JavaScript when
- Type safety is a hard requirement — plain JS has no compile-time type checking. Bugs in production caused by
undefined is not a functionare legendary. Use TypeScript instead for anything beyond a small script. - CPU-bound workloads — JS is not the right tool for video encoding, scientific computing, or cryptography. Use WebAssembly, Rust, or C++ for that layer; call it from JS if needed.
- True multithreading — JS is single-threaded (Web Workers exist but are isolated). For parallel computation, Go, Java, or Rust are better choices.
- Long-lived, strict codebases at scale — without TypeScript and disciplined conventions, large JS codebases become hard to maintain. Prefer TypeScript for any project that will outlive the sprint it was created in.
JavaScript vs. its closest alternatives
- JS vs TypeScript — TypeScript is JS with types. For any project larger than a script, TypeScript will catch bugs earlier and make refactoring safer at almost zero cost. There is rarely a reason to write plain JS in 2024+.
- JS vs Python — both are dynamically typed and interpreted. JS owns the browser and real-time web; Python owns data science and scripting. For a pure backend API either works; Node tends to handle more concurrent connections with less memory.
- JS (Node) vs Go — Go is faster, statically typed, and has real concurrency. Node has a bigger ecosystem and requires less boilerplate for web APIs. Choose Go when performance and correctness are critical; Node when ecosystem breadth matters.
Resources
- MDN Web Docs — the most trusted JS reference and guide
- javascript.info — thorough modern JS tutorial from basics to advanced
- ECMAScript specification — the living language standard
- Node.js docs — server-side JS runtime API reference
- You Don't Know JS — free deep-dive book series by Kyle Simpson
Topics
Variables & Types
// Declaration
let name = 'Alice'; // block-scoped, reassignable
const PI = 3.14159; // block-scoped, not reassignable
// var old = true; // function-scoped — avoid
// Primitive types
let num = 42;
let float = 3.14;
let big = 9007199254740993n; // BigInt
let str = 'hello';
let bool = true;
let nothing = null;
let undef; // undefined
let sym = Symbol('id');
// Type checking
typeof 42 // 'number'
typeof 'hi' // 'string'
typeof null // 'object' (historical quirk)
Array.isArray([]) // true
// Type coercion
'5' + 3 // '53' — string concat
'5' - 3 // 2 — numeric coercion
!!0 // false
!!'' // falseStrings
const name = 'Alice';
// Template literals
const greeting = `Hello, ${name}!`;
const multiline = `
line one
line two
`;
// Common methods
'hello'.toUpperCase() // 'HELLO'
' hello '.trim() // 'hello'
'hello world'.split(' ') // ['hello', 'world']
['a', 'b'].join(', ') // 'a, b'
'hello'.includes('ell') // true
'hello'.startsWith('he') // true
'hello'.replace('l', 'L') // 'heLlo'
'hello'.replaceAll('l', 'L') // 'heLLo'
'hello'.slice(1, 3) // 'el'
'hi'.padStart(5, '0') // '000hi'
'ha'.repeat(3) // 'hahaha'
Number('42') // 42Control Flow
const x = 42;
// if / else
if (x > 100) {
console.log('big');
} else if (x > 10) {
console.log('medium');
} else {
console.log('small');
}
// Ternary
const label = x % 2 === 0 ? 'even' : 'odd';
// Nullish coalescing
const port = null ?? 3000;
const safe = undefined ?? 'default';
// Optional chaining
const city = user?.address?.city;
// switch
switch (x % 3) {
case 0: console.log('divisible'); break;
case 1: console.log('rem 1'); break;
default: console.log('rem 2');
}
// Loops
for (let i = 0; i < 5; i++) { /* ... */ }
for (const item of [1, 2, 3]) { console.log(item); }
for (const key in { a: 1, b: 2 }) { console.log(key); }
// Array iteration
[1,2,3].forEach(x => console.log(x));
[1,2,3].map(x => x * 2);
[1,2,3,4].filter(x => x % 2);
[1,2,3].reduce((acc, x) => acc + x, 0);
[1,2,3].find(x => x > 1);
[1,2,3].every(x => x > 0);
[1,2,3].some(x => x > 2);Functions
// Function declaration (hoisted)
function add(a, b) { return a + b; }
// Arrow function
const square = x => x * x;
const greet = (name, greeting = 'Hello') => `${greeting}, ${name}!`;
// Rest parameters
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
// Spread in call
Math.max(...[1, 2, 3]);
// Destructured parameters with defaults
function createUser({ name, age = 0, role = 'user' } = {}) {
return { name, age, role };
}
// Closure
function counter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
}
// IIFE
const result = (() => {
const x = 42;
return x * 2;
})();Objects & Arrays
const person = {
name: 'Alice',
age: 30,
greet() { return `Hi, I'm ${this.name}`; },
};
// Property access
person.name;
person['age'];
person.city ?? 'unknown'; // nullish fallback
// Dynamic keys
const key = 'email';
const user = { [key]: 'alice@example.com' };
// Object methods
Object.keys(person)
Object.values(person)
Object.entries(person)
const merged = Object.assign({}, person, { age: 31 });
// Spread merge
const updated = { ...person, age: 31 };
// Arrays
const nums = [1, 2, 3];
nums.push(4); // append
nums.pop(); // remove last
nums.shift(); // remove first
nums.unshift(0); // prepend
[...nums].sort((a, b) => a - b);
// Flat
[[1,2],[3,4]].flat() // [1,2,3,4]
[1,2,3].flatMap(x => [x, x*2]) // [1,2,2,4,3,6]Destructuring & Spread
// Array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// Skip elements
const [,, third] = [10, 20, 30];
// Default values
const [a = 0, b = 0] = [1]; // a=1, b=0
// Object destructuring
const { name, age, city = 'unknown' } = person;
// Rename on destructure
const { name: personName } = person;
// Nested
const { address: { street } } = { address: { street: '123 Main' } };
// Function parameter destructuring
function display({ name, age }) {
console.log(`${name} is ${age}`);
}
// Spread
const arr2 = [...nums, 4, 5];
const obj2 = { ...person, role: 'admin' };Classes
class Animal {
#sound; // private field (ES2022)
constructor(name, sound) {
this.name = name;
this.#sound = sound;
}
speak() {
return `${this.name} says ${this.#sound}`;
}
get label() { return `[${this.name}]`; }
set label(v) { this.name = v.slice(1, -1); }
static create(name, sound) {
return new Animal(name, sound);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name, 'Woof');
this.breed = breed;
}
speak() {
return super.speak() + '!';
}
}
const dog = new Dog('Rex', 'Labrador');
dog instanceof Animal; // trueAsync / Await
// Promise
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000);
});
// Promise combinators
Promise.all([p1, p2, p3]) // all must resolve
Promise.allSettled([p1, p2]) // wait for all
Promise.race([p1, p2]) // first to settle
Promise.any([p1, p2]) // first to resolve
// async / await
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('Failed:', err);
throw err;
}
}
// Parallel fetches
async function loadAll(ids) {
return Promise.all(ids.map(id => fetchUser(id)));
}Modules (ESM)
// --- math.js ---
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default class Calculator { /* ... */ }
// --- main.js ---
import Calculator, { PI, add } from './math.js';
import * as math from './math.js';
// Dynamic import (lazy loading)
const { add } = await import('./math.js');
// Re-export
export { add } from './math.js';
export * from './math.js';DOM Basics
// Selecting elements
const el = document.querySelector('#myId');
const els = document.querySelectorAll('.myClass');
// Modifying elements
el.textContent = 'Hello';
el.innerHTML = '<strong>Bold</strong>';
el.setAttribute('data-value', '42');
el.classList.add('active');
el.classList.toggle('hidden');
el.style.color = 'red';
// Creating elements
const div = document.createElement('div');
div.textContent = 'New element';
document.body.appendChild(div);
// Events
el.addEventListener('click', (event) => {
event.preventDefault();
console.log('Clicked!', event.target);
});
// Custom events
el.dispatchEvent(new CustomEvent('my-event', { detail: { value: 42 } }));
el.addEventListener('my-event', e => console.log(e.detail));Iterators & Generators
// Iterator protocol: object with next() returning { value, done }
function makeRangeIterator(start, end) {
let current = start;
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
}
// Custom iterable (implements [Symbol.iterator])
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (const n of range) { console.log(n); } // 1 2 3 4 5
const arr = [...range]; // [1, 2, 3, 4, 5]
// Generator function — pauseable with yield
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
fib.next().value; // 0
fib.next().value; // 1
fib.next().value; // 1
// yield* delegates to another iterable
function* concat(...iters) {
for (const it of iters) yield* it;
}
[...concat([1, 2], [3, 4])]; // [1, 2, 3, 4]
// Async generator + for await...of
async function* paginate(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (!data.length) return;
yield data;
page++;
}
}
for await (const items of paginate('/api/items')) {
console.log(items);
}Symbols & Well-known Symbols
// Symbol() — always unique
const id1 = Symbol('id');
const id2 = Symbol('id');
id1 === id2; // false
// Symbol.for() — shared global registry
const shared = Symbol.for('app.token');
Symbol.for('app.token') === shared; // true
Symbol.keyFor(shared); // 'app.token'
// Symbol as unique object key (never clashes with string keys)
const SECRET = Symbol('secret');
const obj = { name: 'Alice', [SECRET]: 'top-secret' };
Object.keys(obj); // ['name'] — symbol not enumerated
obj[SECRET]; // 'top-secret'
// Symbol.iterator — make any object iterable
class Range {
constructor(from, to) { this.from = from; this.to = to; }
[Symbol.iterator]() {
let n = this.from;
const to = this.to;
return { next: () => n <= to ? { value: n++, done: false } : { done: true } };
}
}
[...new Range(1, 3)]; // [1, 2, 3]
// Symbol.toPrimitive — control type coercion
const money = {
amount: 42,
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount;
if (hint === 'string') return `${this.amount}`;
return this.amount;
},
};
+money; // 42
`${money}`; // '$42'
// Symbol.hasInstance — customise instanceof
class EvenNumber {
static [Symbol.hasInstance](n) { return Number.isInteger(n) && n % 2 === 0; }
}
4 instanceof EvenNumber; // true
3 instanceof EvenNumber; // false
// Symbol.toStringTag — customise Object.prototype.toString
class MyCollection {
get [Symbol.toStringTag]() { return 'MyCollection'; }
}
Object.prototype.toString.call(new MyCollection()); // '[object MyCollection]'Proxy & Reflect
// Basic Proxy: intercept property access
const handler = {
get(target, prop, receiver) {
console.log(`get: ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`set: ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
has(target, prop) {
console.log(`has: ${prop}`);
return Reflect.has(target, prop);
},
deleteProperty(target, prop) {
console.log(`delete: ${prop}`);
return Reflect.deleteProperty(target, prop);
},
};
const target = { x: 1, y: 2 };
const proxy = new Proxy(target, handler);
proxy.x; // logs 'get: x', returns 1
proxy.z = 3; // logs 'set: z = 3'
'x' in proxy; // logs 'has: x'
// Validation proxy
function createValidated(schema) {
return new Proxy({}, {
set(obj, prop, value) {
if (schema[prop] && typeof value !== schema[prop]) {
throw new TypeError(`${prop} must be ${schema[prop]}`);
}
obj[prop] = value;
return true;
},
});
}
const person = createValidated({ name: 'string', age: 'number' });
person.name = 'Alice'; // ok
// person.age = 'old'; // throws TypeError
// Function proxy: intercept apply and construct
const logged = new Proxy(Math.max, {
apply(fn, thisArg, args) {
console.log(`called with ${args}`);
return Reflect.apply(fn, thisArg, args);
},
});
logged(1, 2, 3); // logs 'called with 1,2,3', returns 3
// Reflect.ownKeys — all own keys including symbols
const sym = Symbol('s');
const o = { a: 1, [sym]: 2 };
Reflect.ownKeys(o); // ['a', Symbol(s)]WeakMap, WeakSet & WeakRef
// WeakMap: keys are weakly held (GC eligible when no other ref)
// Use case 1: private data per instance
const _private = new WeakMap();
class Counter {
constructor() { _private.set(this, { count: 0 }); }
increment() { _private.get(this).count++; }
value() { return _private.get(this).count; }
}
// Use case 2: caching computed results without leaking memory
const cache = new WeakMap();
function expensiveProcess(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = JSON.stringify(obj).length; // placeholder
cache.set(obj, result);
return result;
}
// WeakSet: track objects without preventing garbage collection
const seen = new WeakSet();
function processOnce(obj) {
if (seen.has(obj)) return;
seen.add(obj);
console.log('processing', obj);
}
// WeakRef: hold a weak reference to an object
let bigData = { payload: new Array(1e6).fill(0) };
const ref = new WeakRef(bigData);
bigData = null; // allow GC
// later...
const data = ref.deref(); // may be undefined after GC
if (data) { console.log('still alive'); }
// FinalizationRegistry: callback when object is collected
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} was collected`);
});
let obj = { id: 1 };
registry.register(obj, 'obj#1');
obj = null; // eligible for GC — callback fires eventually
// Key difference vs Map/Set:
// Map/Set hold strong refs → prevent GC
// WeakMap/WeakSet hold weak refs → allow GC when no other ref existsError Handling Patterns
// Custom Error subclasses
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = 'AppError';
this.code = code;
}
}
class NetworkError extends AppError {
constructor(message, statusCode) {
super(message, 'NETWORK_ERROR');
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
// Error chaining with cause (ES2022)
try {
JSON.parse('bad json');
} catch (original) {
throw new Error('Config file is corrupt', { cause: original });
}
// AggregateError — multiple errors at once
const errors = [new Error('e1'), new Error('e2')];
throw new AggregateError(errors, 'Multiple failures');
// Type-safe result pattern (no exceptions for expected failures)
function divide(a, b) {
if (b === 0) return { ok: false, error: 'Division by zero' };
return { ok: true, value: a / b };
}
const result = divide(10, 0);
if (!result.ok) { console.error(result.error); }
else { console.log(result.value); }
// Promise rejection vs thrown error in async functions
async function risky() {
throw new Error('sync throw inside async'); // becomes rejection
}
risky().catch(err => console.error(err.message));
// Unhandled rejection handling (global safety net)
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // suppress default console warning
});Design Patterns
// 1. Module pattern — private state via closure
const counter = (() => {
let _count = 0;
return {
increment() { _count++; },
decrement() { _count--; },
value() { return _count; },
};
})();
// 2. Observer / event emitter
class EventEmitter {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
this.#listeners.get(event).push(fn);
return () => this.off(event, fn);
}
off(event, fn) {
const fns = this.#listeners.get(event) ?? [];
this.#listeners.set(event, fns.filter(f => f !== fn));
}
emit(event, ...args) {
(this.#listeners.get(event) ?? []).forEach(fn => fn(...args));
}
}
// 3. Singleton
const db = (() => {
let instance;
return { getInstance: () => (instance ??= { connected: false }) };
})();
// 4. Factory function vs class (factory avoids new / prototype chain)
function createUser(name, role = 'user') {
return { name, role, greet() { return `Hi, I'm ${name}`; } };
}
// 5. Mixin — compose behaviour without inheritance
const Serializable = (Base) => class extends Base {
serialize() { return JSON.stringify(this); }
};
class Point { constructor(x, y) { this.x = x; this.y = y; } }
class SerializablePoint extends Serializable(Point) {}
// 6. Command pattern — encapsulate operations for undo/redo
function makeTextEditor() {
let text = '';
const history = [];
return {
execute(cmd) { history.push(cmd); cmd.execute(); },
undo() { history.pop()?.undo(); },
getText() { return text; },
insertCmd(str) {
return {
execute() { text += str; },
undo() { text = text.slice(0, -str.length); },
};
},
};
}Performance Tips
// 1. Avoid layout thrashing — batch reads then writes
// Bad: interleaved read/write forces repeated reflow
// el.style.left = el.offsetLeft + 1 + 'px'; // read then write in loop
// Good: read all first, then write all
const positions = elements.map(el => el.offsetLeft);
elements.forEach((el, i) => { el.style.left = positions[i] + 1 + 'px'; });
// 2. DocumentFragment — batch DOM insertions
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
frag.appendChild(li);
}
list.appendChild(frag); // single reflow
// 3. requestAnimationFrame — sync animations to display refresh
function animate(timestamp) {
el.style.transform = `translateX(${timestamp * 0.1 % 300}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// 4. requestIdleCallback — defer non-urgent work
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length) {
tasks.shift()();
}
}, { timeout: 2000 });
// 5. IntersectionObserver — lazy load without scroll events
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
}, { rootMargin: '100px' });
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
// 6. structuredClone vs JSON round-trip
const clone = structuredClone(obj); // handles Date, Map, Set, undefined
// JSON.parse(JSON.stringify(obj)) // loses Date, Map, Set, undefined
// 7. Object.create(null) — pure dictionary (no prototype overhead)
const dict = Object.create(null);
dict['key'] = 'value';
'toString' in dict; // false — no inherited keys
// 8. WeakMap to avoid memory leaks
const cache = new WeakMap(); // entries GC'd when key object is collected
// Map would keep keys alive indefinitely
// 9. Microtask vs macrotask ordering
setTimeout(() => console.log('macrotask'), 0);
Promise.resolve().then(() => console.log('microtask'));
queueMicrotask(() => console.log('microtask 2'));
// order: microtask, microtask 2, macrotaskClosures & Scope
A closure is a function paired with its lexical environment — the variables in scope at the time of definition, not execution. Closures enable data encapsulation, the module pattern, partial application, and memoization.
// Closure — a function that captures its surrounding lexical scope
function makeCounter(start = 0) {
let count = start; // captured in the closure
return {
increment() { return ++count; },
decrement() { return --count; },
value() { return count; },
};
}
const counter = makeCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.decrement(); // 11
// Classic loop-variable capture bug (var) vs. fix (let / IIFE)
// Bug: all callbacks share the same `i`
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // logs 3 3 3
}
// Fix 1: let — each iteration gets its own binding
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // logs 0 1 2
}
// Fix 2: IIFE — create a new scope per iteration
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 0);
})(i);
}
// Module pattern — private state via closure
const store = (function() {
let _state = {};
return {
get: (k) => _state[k],
set: (k, v) => { _state[k] = v; },
};
})();
// Memoize — cache results using closure
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;
};
}Event Loop & Microtasks
JavaScript is single-threaded. The event loop processes one task at a time: it runs all synchronous code, drains the microtask queue (Promises, queueMicrotask) completely, then picks the next macrotask (setTimeout, I/O). Long synchronous work blocks everything — break it into chunks.
// JavaScript is single-threaded with a cooperative event loop.
// Execution order: synchronous → microtasks → macrotasks (tasks)
console.log('1 sync');
setTimeout(() => console.log('4 macrotask'), 0); // task queue
Promise.resolve().then(() => console.log('2 microtask'));
queueMicrotask(() => console.log('3 microtask'));
console.log('1 sync end');
// Output: '1 sync' → '1 sync end' → '2 microtask' → '3 microtask' → '4 macrotask'
// Microtask queue drains completely before the next macrotask
Promise.resolve()
.then(() => { console.log('A'); return Promise.resolve(); })
.then(() => console.log('B'));
Promise.resolve().then(() => console.log('C'));
// A → C → B (each .then enqueues the next microtask at the tail)
// requestAnimationFrame — fires before the next paint (between tasks)
requestAnimationFrame(() => console.log('before paint'));
// Long tasks block the event loop — break them up
function processChunk(items, index = 0) {
const CHUNK = 100;
for (let i = index; i < Math.min(index + CHUNK, items.length); i++) {
// process items[i]
}
if (index + CHUNK < items.length) {
setTimeout(() => processChunk(items, index + CHUNK), 0); // yield
}
}
// scheduler.postTask (modern browsers) — prioritised tasks
// scheduler.postTask(() => heavyWork(), { priority: 'background' });Regular Expressions
Regular expressions in JavaScript use the RegExp object or literal /pattern/flags syntax. Named capture groups, lookbehind assertions, and matchAll were added in ES2018.
// Literal syntax and constructor
const re1 = /hello/i; // case-insensitive
const re2 = new RegExp('hello', 'i');
// Flags: g (global), i (ignore case), m (multiline), s (dotAll), u (unicode), d (indices)
// test — true/false
/^\d+$/.test('123'); // true
/^\d+$/.test('12x'); // false
// match — returns array or null
'hello world'.match(/\w+/g); // ['hello', 'world']
'2024-07-15'.match(/(\d{4})-(\d{2})-(\d{2})/);
// index 0: full match, 1: '2024', 2: '07', 3: '15'
// matchAll — iterator of all matches with capture groups
const str = 'key1=val1 key2=val2';
for (const m of str.matchAll(/(\w+)=(\w+)/g)) {
console.log(m[1], m[2]); // key1 val1 / key2 val2
}
// replace / replaceAll
'hello world'.replace(/o/g, '0'); // 'hell0 w0rld'
'2024-07-15'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1'); // '15/07/2024'
// Named groups (ES2018)
const { year, month, day } =
'2024-07-15'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/).groups;
// Lookahead and lookbehind
/\d+(?= dollars)/.exec('100 dollars'); // '100' (positive lookahead)
/(?<=\$)\d+/.exec('$99'); // '99' (positive lookbehind)
// split with capturing group
'a1b2c'.split(/(\d)/); // ['a', '1', 'b', '2', 'c']Web Workers
Web Workers run JavaScript on a background thread without blocking the main thread. Communication uses postMessage / onmessage with structured-clone serialization. SharedArrayBuffer + Atomics enable true shared memory between threads.
// ── main.js ──────────────────────────────────────────────────────────────
const worker = new Worker('./worker.js');
// Send data to worker (structured clone — no shared memory by default)
worker.postMessage({ cmd: 'compute', data: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => console.log('result:', e.data);
worker.onerror = (e) => console.error(e.message);
// Terminate when done
// worker.terminate();
// ── worker.js ─────────────────────────────────────────────────────────────
self.onmessage = (e) => {
const { cmd, data } = e.data;
if (cmd === 'compute') {
const sum = data.reduce((a, b) => a + b, 0);
self.postMessage(sum);
}
};
// ── Shared memory with SharedArrayBuffer ──────────────────────────────────
// Requires COOP/COEP headers: Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
const sab = new SharedArrayBuffer(4); // 4 bytes
const shared = new Int32Array(sab); // typed view
worker.postMessage({ sab });
// In worker.js — Atomics: thread-safe operations on shared memory
// self.onmessage = ({ data: { sab } }) => {
// const arr = new Int32Array(sab);
// Atomics.add(arr, 0, 1); // atomic increment
// Atomics.notify(arr, 0, 1); // wake up any Atomics.wait
// };
// Atomics.wait (worker) / Atomics.waitAsync (main thread, non-blocking)
// Atomics.wait(shared, 0, 0); // block until shared[0] !== 0Intl API
The Intl API provides locale-aware formatting for numbers, dates, relative times, lists, and plurals. It is built into every modern runtime — no library needed.
// Intl.NumberFormat
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
usd.format(1234567.89); // '$1,234,567.89'
const pct = new Intl.NumberFormat('en', { style: 'percent', maximumFractionDigits: 1 });
pct.format(0.1234); // '12.3%'
// Intl.DateTimeFormat
const df = new Intl.DateTimeFormat('fr-FR', { dateStyle: 'full', timeStyle: 'short' });
df.format(new Date()); // 'lundi 15 juillet 2024 à 14:30'
// Intl.RelativeTimeFormat
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // 'yesterday'
rtf.format(2, 'week'); // 'in 2 weeks'
// Intl.ListFormat
const lf = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
lf.format(['Alice', 'Bob', 'Charlie']); // 'Alice, Bob, and Charlie'
// Intl.Collator — locale-aware string sorting
const words = ['résumé', 'apple', 'éclair', 'banana'];
words.sort(new Intl.Collator('fr').compare);
// ['apple', 'banana', 'éclair', 'résumé']
// Intl.PluralRules — choose the right plural form
const pr = new Intl.PluralRules('en-US');
pr.select(0); // 'other'
pr.select(1); // 'one'
pr.select(2); // 'other'
// Intl.Segmenter (ES2022) — split by grapheme, word, or sentence
const seg = new Intl.Segmenter('en', { granularity: 'word' });
[...seg.segment('Hello world!')].map(s => s.segment);
// ['Hello', ' ', 'world', '!']Prototype Chain
Every JavaScript object has an internal [[Prototype]] link to another object (or null). Property lookup traverses this chain until the property is found or the chain ends. class syntax is syntactic sugar over the same prototype mechanism.
// Every object has an internal [[Prototype]] link.
// Property lookup walks the chain until null is reached.
const animal = {
breathe() { return 'breathing'; },
};
const dog = Object.create(animal); // dog.__proto__ === animal
dog.bark = function() { return 'woof'; };
dog.bark(); // own property — found immediately
dog.breathe(); // not own — found on animal via prototype chain
// Object.getPrototypeOf is the spec-safe way to read [[Prototype]]
Object.getPrototypeOf(dog) === animal; // true
// Constructor functions and .prototype
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() { return 'Hi, I\'m ' + this.name; };
const alice = new Person('Alice');
alice.greet(); // found on Person.prototype
// hasOwnProperty vs in
alice.hasOwnProperty('name'); // true
alice.hasOwnProperty('greet'); // false — lives on prototype
'greet' in alice; // true — checked up the chain
// Class syntax desugars to the same prototype mechanism
class Vehicle {
constructor(make) { this.make = make; }
describe() { return 'Vehicle: ' + this.make; }
}
class Car extends Vehicle {
constructor(make, model) {
super(make);
this.model = model;
}
describe() { return super.describe() + ' ' + this.model; }
}
const c = new Car('Toyota', 'Camry');
Object.getPrototypeOf(c) === Car.prototype; // true
Object.getPrototypeOf(Car.prototype) === Vehicle.prototype; // true
// Object.create(null) — no prototype at all (pure hash map)
const map = Object.create(null);
'toString' in map; // false
// Mixin pattern — copy methods without deep inheritance
const Flyable = {
fly() { return this.name + ' is flying'; },
};
Object.assign(Car.prototype, Flyable);
c.fly(); // 'Toyota is flying'this & Binding
this is not lexically scoped — it is resolved at call time based on how a function is invoked: implicit (dot notation), explicit (call/apply/bind), new, or default. Arrow functions are the exception: they capture this from the surrounding lexical scope and cannot be rebound.
// 'this' is determined at call time, not definition time (except arrows).
// 1. Implicit binding — the object left of the dot
const obj = {
name: 'obj',
greet() { return 'Hello from ' + this.name; },
};
obj.greet(); // 'Hello from obj'
// 2. Explicit binding — call / apply / bind
function greet(greeting) { return greeting + ', ' + this.name; }
greet.call({ name: 'Alice' }, 'Hi'); // 'Hi, Alice'
greet.apply({ name: 'Bob' }, ['Hey']); // 'Hey, Bob'
const greetAlice = greet.bind({ name: 'Alice' });
greetAlice('Hi'); // 'Hi, Alice'
// 3. new binding — `this` is the newly created object
function User(name) { this.name = name; }
const u = new User('Carol'); // u.name === 'Carol'
// 4. Default binding — global (or undefined in strict mode)
// function loose() { return this; } // window in browser, undefined strict
// 5. Arrow functions — lexical `this`, cannot be rebound
class Timer {
constructor() { this.ticks = 0; }
start() {
// Arrow captures `this` from start()'s context
this.interval = setInterval(() => { this.ticks++; }, 1000);
}
stop() { clearInterval(this.interval); }
}
// 6. Losing `this` — common mistake with callbacks
class Logger {
constructor(prefix) { this.prefix = prefix; }
log(msg) { console.log(this.prefix + msg); }
attach(btn) {
// btn.addEventListener('click', this.log); // `this` lost
btn.addEventListener('click', this.log.bind(this)); // fixed
btn.addEventListener('click', (e) => this.log(e.type)); // arrow fix
}
}
// 7. Getter / setter `this`
const rect = {
width: 10,
height: 5,
get area() { return this.width * this.height; },
};
rect.area; // 50Functional Programming
Functional programming in JavaScript favours pure functions, immutable data, and composing small transformations. Higher-order functions, currying, partial application, and pipe/compose are the core building blocks for writing predictable, testable code without side effects.
// Pure functions — same input always returns same output, no side effects
const add = (a, b) => a + b;
const double = x => x * 2;
// Immutable updates instead of mutation
const arr = [1, 2, 3];
const arr2 = [...arr, 4]; // new array
const obj = { a: 1, b: 2 };
const obj2 = { ...obj, b: 99 }; // new object
// Higher-order functions — functions that accept or return functions
const multiplyBy = factor => x => x * factor;
const triple = multiplyBy(3);
triple(5); // 15
// Compose — apply functions right-to-left
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const process = pipe(
x => x * 2,
x => x + 1,
x => x.toString(),
);
process(5); // '11'
// Currying — transform f(a,b,c) into f(a)(b)(c)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args);
return (...more) => curried(...args, ...more);
};
}
const curriedAdd = curry((a, b, c) => a + b + c);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
// Partial application
const partial = (fn, ...preset) => (...later) => fn(...preset, ...later);
const addTen = partial(add, 10);
addTen(5); // 15
// Functor-style map over a Maybe
const Maybe = value => ({
map: fn => value == null ? Maybe(null) : Maybe(fn(value)),
getOrElse: fallback => value ?? fallback,
});
Maybe(5).map(double).map(triple).getOrElse(0); // 30
Maybe(null).map(double).getOrElse(0); // 0
// Transducer — composable array transformation without intermediate arrays
const transduce = (xf, reducer, init, coll) =>
coll.reduce(xf(reducer), init);
const mapXf = fn => step => (acc, x) => step(acc, fn(x));
const filterXf = pred => step => (acc, x) => pred(x) ? step(acc, x) : acc;
const xform = compose(filterXf(x => x % 2 === 0), mapXf(x => x * 3));
transduce(xform, (acc, x) => [...acc, x], [], [1,2,3,4,5,6]);
// [6, 12, 18]Streams API
The Streams API lets you process data incrementally — read, transform, and write in chunks — without loading the entire payload into memory. ReadableStream, WritableStream, and TransformStream compose via pipeThrough and pipeTo. The Fetch API exposes response bodies as ReadableStream.
// Streams API — process data incrementally without buffering everything
// ReadableStream — produce data chunk by chunk
const readable = new ReadableStream({
start(controller) {
controller.enqueue('Hello ');
controller.enqueue('world');
controller.close();
},
});
// Consume a ReadableStream with a reader
const reader = readable.getReader();
const chunks = [];
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
}
console.log(chunks.join('')); // 'Hello world'
// WritableStream — accept data chunk by chunk
const writable = new WritableStream({
write(chunk) { console.log('received:', chunk); },
close() { console.log('stream closed'); },
abort(err) { console.error('aborted:', err); },
});
// TransformStream — read one side, write the other (pipeable middleware)
const uppercase = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
});
// Pipe chain: readable -> transform -> writable
await readable
.pipeThrough(uppercase)
.pipeTo(writable);
// Fetch body is a ReadableStream — stream a large download
const response = await fetch('/large-file.bin');
const streamReader = response.body.getReader();
let received = 0;
while (true) {
const { value, done } = await streamReader.read();
if (done) break;
received += value.byteLength;
console.log(`Received ${received} bytes`);
}
// TextDecoderStream / TextEncoderStream — convert bytes to text in a pipe
const textStream = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, ctrl) { ctrl.enqueue(chunk.toUpperCase()); },
}));Fetch & Request/Response
The Fetch API replaces XMLHttpRequest with a Promise-based, stream-aware interface. Request and Response are first-class objects, enabling middleware-style patterns. AbortController provides cancellation, and the body is a ReadableStream you can consume once.
// Basic GET
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const users = await res.json();
// POST with JSON body
const created = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice', role: 'admin' }),
});
// Request object — reusable, inspectable
const req = new Request('/api/data', {
method: 'PUT',
headers: new Headers({
'Authorization': 'Bearer token123',
'Accept': 'application/json',
}),
body: JSON.stringify({ value: 42 }),
cache: 'no-store',
credentials: 'include',
});
const r2 = await fetch(req);
// Response methods
res.json(); // parse body as JSON
res.text(); // parse body as text
res.blob(); // parse body as Blob
res.arrayBuffer(); // parse body as ArrayBuffer
res.formData(); // parse body as FormData
// Clone before consuming (body can only be read once)
const clone = res.clone();
const text = await clone.text();
// AbortController — cancel in-flight requests
const ac = new AbortController();
setTimeout(() => ac.abort(), 5000); // timeout after 5 s
try {
const data = await fetch('/slow-api', { signal: ac.signal });
} catch (err) {
if (err.name === 'AbortError') console.log('Request cancelled');
else throw err;
}
// Retry helper
async function fetchWithRetry(url, opts = {}, retries = 3) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const res = await fetch(url, opts);
if (res.ok || attempt === retries) return res;
} catch (err) {
if (attempt === retries) throw err;
await new Promise(r => setTimeout(r, 2 ** attempt * 100));
}
}
}Web Components
Web Components are a suite of browser standards — Custom Elements, Shadow DOM, and HTML Templates — that let you define reusable, encapsulated HTML elements with their own styles and behaviour, without any framework dependency.
// Custom Element — autonomous
class MyCard extends HTMLElement {
static observedAttributes = ['title', 'open'];
constructor() {
super();
// Shadow DOM — encapsulated subtree
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
render() {
const title = this.getAttribute('title') ?? 'Untitled';
const open = this.hasAttribute('open');
this.shadowRoot.innerHTML = `
<style>
:host { display: block; border: 1px solid #ccc; border-radius: 4px; }
header { padding: 8px 12px; background: #f5f5f5; font-weight: bold; }
section { display: ${open ? 'block' : 'none'}; padding: 12px; }
</style>
<header>${title}</header>
<section><slot></slot></section>
`;
this.shadowRoot.querySelector('header')
.addEventListener('click', () => this.toggleAttribute('open'));
}
}
customElements.define('my-card', MyCard);
// Customized built-in element (extends existing element)
class FancyButton extends HTMLButtonElement {
connectedCallback() {
this.style.borderRadius = '4px';
}
}
customElements.define('fancy-button', FancyButton, { extends: 'button' });
// HTML Templates — inert, cloneable markup
const template = document.createElement('template');
template.innerHTML = `
<style>span { color: salmon; }</style>
<span><slot name='label'>default</slot></span>
`;
class TagBadge extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' })
.appendChild(template.content.cloneNode(true));
}
}
customElements.define('tag-badge', TagBadge);
// Usage in HTML:
// <my-card title='Hello' open>
// <p>Card body goes here</p>
// </my-card>
// <tag-badge><span slot='label'>ES2024</span></tag-badge>Typed Arrays & Buffers
Typed arrays give direct access to raw binary memory via an ArrayBuffer. Fixed-size element views (Int32Array, Float64Array, etc.) and the flexible DataView enable high-performance binary data handling for audio, images, WebGL, networking, and file I/O.
// Typed arrays wrap an ArrayBuffer — raw binary memory
const buffer = new ArrayBuffer(16); // 16 bytes
const view = new DataView(buffer);
// DataView — read/write any type at any byte offset (endian-aware)
view.setInt32(0, 0xDEADBEEF, false); // big-endian
view.getInt32(0, false); // -559038737 (0xDEADBEEF signed)
view.setFloat64(8, Math.PI, true); // little-endian
view.getFloat64(8, true); // 3.141592653589793
// Typed array views — each element is a fixed type
const i32 = new Int32Array(buffer); // 4 elements (4 bytes each)
const f32 = new Float32Array(4); // allocates own 16-byte buffer
const u8 = new Uint8Array(buffer); // 16 elements (1 byte each)
// Shared underlying buffer
const original = new Int16Array([1, 2, 3, 4]);
const alias = new Uint8Array(original.buffer); // same memory
// Creating typed arrays
const a = new Float32Array([1.5, 2.5, 3.5]);
const b = Float32Array.from([1, 2, 3], x => x * 0.5);
const c = new Int32Array(8); // zeroed, length 8
const d = new Uint8Array(a.buffer, 4, 4); // offset 4, 4 bytes
// Typed array API (mostly mirrors Array)
a.length; // 3
a.byteLength; // 12
a.byteOffset; // 0
a.buffer; // underlying ArrayBuffer
a.set([10, 20]); // overwrite from index 0
a.subarray(1, 3); // view without copy
a.slice(0, 2); // new typed array with copy
a.sort(); // in-place numeric sort
a.fill(0); // zero out
// Blob / File to ArrayBuffer
async function fileToBuffer(file) {
return file.arrayBuffer();
}
// TextEncoder / TextDecoder
const enc = new TextEncoder();
const bytes = enc.encode('Hello'); // Uint8Array
const dec = new TextDecoder('utf-8');
dec.decode(bytes); // 'Hello'Web Storage & IndexedDB
localStorage and sessionStorage offer synchronous string key-value storage capped at ~5 MB. IndexedDB is the browser's full async object database: it supports structured data, indexes, cursors, and multi-object-store transactions with significantly higher storage limits.
// localStorage / sessionStorage — synchronous, string key-value
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // 'dark'
localStorage.removeItem('theme');
localStorage.clear();
// Store objects by JSON-serialising
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));
const user = JSON.parse(localStorage.getItem('user'));
// sessionStorage — same API, cleared when tab closes
sessionStorage.setItem('draft', 'unsaved content');
// Storage event — fired in OTHER tabs/windows on same origin
window.addEventListener('storage', (e) => {
console.log(e.key, e.oldValue, e.newValue, e.storageArea);
});
// ── IndexedDB — async, structured, supports indexes & transactions ──────────
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
store.createIndex('by_name', 'name', { unique: false });
};
request.onsuccess = (e) => {
const db = e.target.result;
// Write
const tx = db.transaction('users', 'readwrite');
tx.objectStore('users').add({ name: 'Alice', age: 30 });
tx.commit();
// Read by key
const tx2 = db.transaction('users', 'readonly');
const get = tx2.objectStore('users').get(1);
get.onsuccess = () => console.log(get.result);
// Cursor over index
const tx3 = db.transaction('users', 'readonly');
const idx = tx3.objectStore('users').index('by_name');
idx.openCursor().onsuccess = function() {
const cursor = this.result;
if (!cursor) return;
console.log(cursor.value);
cursor.continue();
};
};
// Promise wrapper for concise async usage
function idbGet(db, storeName, key) {
return new Promise((resolve, reject) => {
const req = db.transaction(storeName).objectStore(storeName).get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}Dynamic Import & Code Splitting
The import() expression loads an ES module on demand and returns a Promise. Bundlers (Vite, webpack, Rollup) use dynamic imports as split points to emit separate chunks, so users only download the code they actually need. import.meta exposes module-level metadata including the module's URL.
// Static import — resolved at parse time, always hoisted
import { format } from './date-utils.js';
// Dynamic import() — returns a Promise, evaluated at runtime
const { format } = await import('./date-utils.js');
// Conditional loading — only pay for what you use
async function loadChart(type) {
if (type === 'bar') {
const { BarChart } = await import('./charts/bar.js');
return new BarChart();
}
const { LineChart } = await import('./charts/line.js');
return new LineChart();
}
// Route-based code splitting (e.g. with a router)
const routes = {
'/': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js'),
'/contact': () => import('./pages/Contact.js'),
};
async function navigate(path) {
const load = routes[path] ?? routes['/'];
const { default: Page } = await load();
document.getElementById('app').replaceChildren(new Page().render());
}
// Prefetch — hint the browser to load a module before it's needed
function prefetch(specifier) {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = specifier;
document.head.appendChild(link);
}
prefetch('./pages/About.js'); // load now, use later
// import.meta — module metadata
console.log(import.meta.url); // absolute URL of the current module
const assetUrl = new URL('./logo.svg', import.meta.url).href;
// Top-level await — module execution pauses until resolved
// (works at the top level of an ES module, not inside functions)
const config = await fetch('/api/config').then(r => r.json());
export const API_BASE = config.apiBase;
// Dynamic import with error handling
async function safeDynImport(specifier) {
try {
return await import(specifier);
} catch (err) {
console.error('Failed to load module:', specifier, err);
return null;
}
}Observable Patterns
Observables model lazy, push-based event streams — they do nothing until subscribed and can be composed with operators (map, filter, merge). A Subject is both an Observable and an Observer, enabling multicast. This pattern underpins RxJS and the TC39 Observable proposal.
// Observable pattern — push-based event stream with lazy evaluation
// 1. Minimal Observable implementation
class Observable {
constructor(subscribe) { this._subscribe = subscribe; }
subscribe(observer) {
const obs = typeof observer === 'function'
? { next: observer, error: console.error, complete: () => {} }
: observer;
return this._subscribe(obs);
}
// Operators return new Observables (composable pipeline)
map(fn) {
return new Observable(obs => {
return this.subscribe({
next: x => obs.next(fn(x)),
error: e => obs.error(e),
complete: () => obs.complete(),
});
});
}
filter(pred) {
return new Observable(obs => {
return this.subscribe({
next: x => pred(x) && obs.next(x),
error: e => obs.error(e),
complete: () => obs.complete(),
});
});
}
static fromEvent(target, event) {
return new Observable(obs => {
const handler = e => obs.next(e);
target.addEventListener(event, handler);
return () => target.removeEventListener(event, handler); // unsubscribe
});
}
static interval(ms) {
return new Observable(obs => {
let n = 0;
const id = setInterval(() => obs.next(n++), ms);
return () => clearInterval(id);
});
}
}
// 2. Subject — both Observable and Observer (multicast)
class Subject extends Observable {
#observers = [];
constructor() {
super(obs => {
this.#observers.push(obs);
return () => { this.#observers = this.#observers.filter(o => o !== obs); };
});
}
next(value) { this.#observers.forEach(o => o.next(value)); }
error(err) { this.#observers.forEach(o => o.error(err)); }
complete() { this.#observers.forEach(o => o.complete()); }
}
// Usage
const clicks = Observable.fromEvent(document, 'click');
const unsubscribe = clicks
.filter(e => e.target.matches('button'))
.map(e => e.target.textContent)
.subscribe(text => console.log('button clicked:', text));
// Tear down later
unsubscribe();Best Practices
Modern JS Idioms
// Prefer const over let over var
const MAX = 100;
let count = 0;
// var x = 1; // avoid — function-scoped, hoisted
// Destructuring
const { name, age = 0 } = user;
const [first, ...rest] = items;
// Optional chaining — no more TypeError on nullish
const city = user?.address?.city;
const first2 = arr?.[0];
const result2 = obj?.method?.();
// Nullish coalescing — only falls back on null/undefined
const port = config.port ?? 3000;
const label = value ?? 'default';
// Logical assignment operators
settings.theme ||= 'light'; // set if falsy
settings.debug &&= isProd; // clear if truthy
cache.entry ??= computeValue(); // set if nullish
// Array methods — prefer over manual loops
const doubled = nums.map(x => x * 2);
const evens = nums.filter(x => x % 2 === 0);
const sum = nums.reduce((acc, x) => acc + x, 0);
const found = users.find(u => u.id === id);
const allActive = users.every(u => u.active);
const anyAdmin = users.some(u => u.role === 'admin');
// Object.entries / fromEntries for transforms
const upper = Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, v.toUpperCase()])
);
// Spread for shallow copy — never mutate in place
const updated = { ...original, status: 'done' };
const merged = [...arr1, ...arr2];
// Promise combinators
await Promise.all([fetchA(), fetchB()]); // all must resolve
await Promise.allSettled([fetchA(), fetchB()]); // partial failures OK
await Promise.race([fetchA(), timeout(5000)]); // first to settle
await Promise.any([mirror1(), mirror2()]); // first to resolve
// globalThis — works in browser, Node, workers
globalThis.setTimeout(() => {}, 0);Functions & Scope
// Prefer pure functions — no side effects, deterministic output
const add = (a, b) => a + b; // pure
// let total = 0; function addSide(x) { total += x; } // impure
// Arrow functions for callbacks — shorter, no own `this`
const doubled = nums.map(n => n * 2);
const evens = nums.filter(n => n % 2 === 0);
// Avoid `this` in regular functions when using as callbacks
// element.addEventListener('click', function() { this... }); // fragile
element.addEventListener('click', () => doWork()); // safer
// Default parameters — document intent, avoid || hacks
function greet(name, greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
// Rest params over the `arguments` object
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
// function oldSum() { [].reduce.call(arguments, ...) } // avoid
// Closure for private state
function makeCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
value: () => count,
};
}
// Memoize expensive computations
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const fib = memoize(n => (n <= 1 ? n : fib(n - 1) + fib(n - 2)));
// Avoid function hoisting surprises — use const for function expressions
const process = (x) => x * 2; // not callable before this line
// function process(x) { ... } // hoisted — callable anywhere in scope
// Named functions for readable stack traces
const handleClick = function handleClick(e) { e.preventDefault(); };Async Patterns
// Always await or .catch() — never ignore Promise rejections
const data = await fetchData(); // await
fetchData().then(use).catch(handleError); // chain
// Prefer async/await over raw .then() chains
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Handle errors at every async boundary
async function renderProfile(id) {
try {
const user = await loadUser(id);
render(user);
} catch (err) {
showError(err.message);
}
}
// AbortController for cancellable fetch
function fetchWithTimeout(url, ms = 5000) {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), ms);
return fetch(url, { signal: ac.signal })
.finally(() => clearTimeout(timer));
}
// Promise.allSettled when partial failures are OK
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
// Avoid mixing callbacks and Promises
// Bad: new Promise(resolve => fs.readFile(path, (err, d) => resolve(d)))
// Good: use fs.promises.readFile(path) directly
// queueMicrotask for scheduling — runs before next macrotask
queueMicrotask(() => {
// Runs after current sync code, before setTimeout
notifySubscribers();
});
// Global unhandled rejection safety net
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled rejection:', e.reason);
});Objects & Immutability
// Object.freeze for true constants
const CONFIG = Object.freeze({
API_BASE: 'https://api.example.com',
TIMEOUT: 5000,
});
// CONFIG.TIMEOUT = 9999; // silently ignored (throws in strict mode)
// structuredClone for deep copy — handles Date, Map, Set, undefined
const clone = structuredClone(original);
// JSON round-trip loses Dates, Maps, Sets, undefined — avoid for those
// Never mutate function arguments
function normalize(user) {
return { ...user, name: user.name.trim() }; // new object
// user.name = user.name.trim(); // mutates caller's object!
}
// Map over plain object for dynamic keys
const counts = new Map();
counts.set('alice', 1);
counts.set('bob', 2);
counts.get('alice'); // 1 — no prototype collision risk
// { alice: 1 }['constructor'] // inherited — avoid for untrusted keys
// Set for unique values
const unique = [...new Set(duplicates)];
const tags = new Set(['js', 'ts', 'js']); // Set { 'js', 'ts' }
// Avoid prototype pollution
// obj.__proto__ = evil; // never
// Object.assign(target, untrusted); // risk if untrusted has __proto__
const safe = Object.assign(Object.create(null), untrusted); // safe merge
// Object.hasOwn over hasOwnProperty (works on null-proto objects)
if (Object.hasOwn(obj, 'key')) { /* ... */ }
// obj.hasOwnProperty('key') // fails on Object.create(null) objects
// WeakMap for private instance data
const _state = new WeakMap();
class Store {
constructor(init) { _state.set(this, { ...init }); }
get(key) { return _state.get(this)[key]; }
}Error Handling
// Always throw Error objects — not strings
throw new Error('Something went wrong');
// throw 'oops'; // no stack trace, instanceof checks fail
// Add meaningful messages
throw new Error(`User ${id} not found in region ${region}`);
// Custom error subclasses for structured handling
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = 'AppError';
this.code = code;
}
}
class NetworkError extends AppError {
constructor(message, status) {
super(message, 'NETWORK_ERROR');
this.name = 'NetworkError';
this.statusCode = status;
}
}
// Never swallow errors silently
try {
riskyOp();
} catch (err) {
// console.log(err); // don't silently swallow
logger.error('riskyOp failed', { err });
throw err; // re-throw if caller needs to know
}
// Global safety nets
window.addEventListener('error', (e) => {
logger.error('Uncaught error', { message: e.message, filename: e.filename });
});
window.addEventListener('unhandledrejection', (e) => {
logger.error('Unhandled rejection', { reason: e.reason });
e.preventDefault();
});
// try/catch in every async function
async function save(data) {
try {
await db.write(data);
} catch (err) {
throw new AppError(`Save failed: ${err.message}`, 'DB_WRITE_ERROR');
}
}
// Error boundaries in UI — wrap risky render paths
function safeRender(component, fallback) {
try {
return component.render();
} catch (err) {
console.error('Render error:', err);
return fallback;
}
}Performance
// Avoid layout thrashing — batch DOM reads before writes
// Bad: read offsetLeft, write style.left, read, write... (forces reflow each time)
const positions = elements.map(el => el.offsetLeft); // read phase
elements.forEach((el, i) => { // write phase
el.style.left = (positions[i] + 1) + 'px';
});
// Debounce high-frequency handlers (resize, scroll, input)
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
window.addEventListener('resize', debounce(recalcLayout, 150));
// Throttle for rate-limited handlers (scroll position, mouse move)
function throttle(fn, ms) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= ms) { last = now; fn(...args); }
};
}
// requestAnimationFrame for smooth animations
function animate(timestamp) {
el.style.transform = `translateX(${(timestamp * 0.05) % 400}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// Prefer CSS transitions over JS animations when possible
// el.style.transition = 'transform 0.3s ease'; then toggle a class
// Cache DOM queries — querySelector is not free
const list = document.getElementById('list'); // once
items.forEach(item => list.appendChild(buildRow(item)));
// DocumentFragment for bulk DOM insertions — single reflow
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement('li');
li.textContent = item.label;
frag.appendChild(li);
}
list.appendChild(frag);
// Virtualize long lists — render only visible rows (e.g. virtual-scroller)
// Remove event listeners to prevent memory leaks
const handler = () => doWork();
el.addEventListener('click', handler);
// later:
el.removeEventListener('click', handler);
// Web Workers for CPU-intensive work — keeps main thread responsive
const worker = new Worker('./heavy.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => renderResult(e.data);