Getting Started
Hello World & Comments
JavaScript runs in browsers and Node.js. console.log() is the primary debug output. JSDoc comments (/** */) provide type info and documentation for IDEs. Use 'use strict' or ES modules for safer parsing. Comments are ignored at runtime.
// Single-line comment
/* Multi-line
comment */
// Console output
console.log("Hello, World!");
console.warn("Warning message");
console.error("Error message");
console.table([{a: 1}, {a: 2}]);
// JSDoc comments (for documentation)
/**
* Adds two numbers
* @param {number} a - First number
* @param {number} b - Second number
* @returns {number} The sum
*/
function add(a, b) { return a + b; }Strict Mode & Modules
'use strict' enables stricter parsing, catching silent errors. ES Modules (import/export) are the modern standard; CommonJS (require/module.exports) is Node.js traditional. Always use modules to avoid polluting the global scope. Browsers support <script type='module'>.
"use strict"; // enables strict mode (catches common mistakes)
x = 10; // ReferenceError: x is not defined (strict mode)
// ES Modules (modern)
// import { greet } from './utils.js';
// export function greet(name) { return `Hello, ${name}`; }
// CommonJS (Node.js traditional)
// const fs = require('fs');
// module.exports = { greet: (name) => `Hello, ${name}` };
// Running JS
// Browser: include in <script> or via dev server
// Node.js: node script.js
// Deno: deno run script.tsVariables: let, const, var
Always prefer const; use let only when reassignment is needed; avoid var entirely. const prevents reassignment but not mutation — object/array contents can still change. let/const are block-scoped; var is function-scoped and hoisted (causing bugs). TDZ prevents using variables before declaration.
// const - cannot be reassigned (use by default)
const PI = 3.14159;
const user = { name: "Alice" };
user.name = "Bob"; // OK - object contents can change
// user = {}; // TypeError - can't reassign const
// let - block-scoped, can be reassigned
let count = 0;
count = 1;
if (true) {
let local = 5; // only accessible in this block
}
// var - function-scoped, hoisted (AVOID in modern code)
var old = "legacy"; // hoisted, can cause bugs
// Temporal Dead Zone (TDZ)
// console.log(x); // ReferenceError
// let x = 5;Data Types & typeof
JavaScript has 7 primitive types (string, number, bigint, boolean, undefined, null, symbol) and reference types (objects, arrays, functions). Primitives are immutable and copied by value; objects are mutable and passed by reference. typeof null returns 'object' due to a historical bug — use === null to check for null.
// Primitive types (immutable, passed by value)
const str = "hello"; // string
const num = 42; // number (no separate int/float)
const big = 9007199254740993n; // bigint
const bool = true; // boolean
const undef = undefined; // undefined (no value)
const empty = null; // null (intentional empty)
const sym = Symbol("id"); // symbol (unique identifier)
// Reference types (mutable, passed by reference)
const arr = [1, 2, 3]; // object (array)
const obj = { a: 1 }; // object
const fn = function() {}; // object (function)
// typeof operator
console.log(typeof "hi"); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (historical bug!)
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
console.log(typeof function(){}); // "function"Type Conversion & Coercion
JavaScript's type coercion is notoriously confusing. Always use === (strict equality) instead of == (loose) to avoid unexpected coercion. The + operator concatenates if either operand is a string; other operators coerce to number. Falsy values: false, 0, '', null, undefined, NaN. Use Number.isNaN() to check for NaN (NaN !== NaN).
// Explicit conversion
String(42); // "42"
(42).toString(); // "42"
Number("42"); // 42
Number("3.14"); // 3.14
Number(""); // 0
Number("abc"); // NaN
parseInt("42px"); // 42
parseFloat("3.14abc"); // 3.14
Boolean(0); // false
Boolean(""); // false
Boolean("x"); // true
// Implicit coercion (often confusing)
console.log("5" + 3); // "53" (string concatenation)
console.log("5" - 3); // 2 (numeric subtraction)
console.log("5" * "2"); // 10
console.log(1 + "2" + 3); // "123"
console.log(true + 1); // 2
// Falsy values: false, 0, "", null, undefined, NaN
// Everything else is truthy
if ("0") console.log("truthy"); // runs! non-empty string
// Strict vs loose equality
console.log(1 == "1"); // true (loose, coerces)
console.log(1 === "1"); // false (strict, no coercion)
console.log(null == undefined); // true
console.log(null === undefined); // falseStrings
String Methods
Strings are immutable — methods return new strings. slice() supports negative indices (from end); substring() doesn't. replace() replaces first match only; use replaceAll() (ES2021) for all. at() (ES2022) allows negative indices. split() + join() is the idiomatic way to replace characters in a string.
const s = "Hello, World";
// Length & access
console.log(s.length); // 12
console.log(s[0]); // "H"
console.log(s.charAt(0)); // "H"
console.log(s.at(-1)); // "d" (ES2022, negative index)
// Case
console.log(s.toUpperCase()); // "HELLO, WORLD"
console.log(s.toLowerCase()); // "hello, world"
// Search
console.log(s.indexOf("World")); // 7 (-1 if not found)
console.log(s.includes("World")); // true
console.log(s.startsWith("Hello")); // true
console.log(s.endsWith("World")); // true
// Extract
console.log(s.slice(0, 5)); // "Hello"
console.log(s.slice(-5)); // "World"
console.log(s.substring(0, 5)); // "Hello" (no negative)
console.log(s.split(", ")); // ["Hello", "World"]
// Modify
console.log(s.replace("o", "0")); // "Hell0, World"
console.log(s.replaceAll("o", "0")); // "Hell0, W0rld"
console.log(s.trim()); // remove whitespace
console.log(s.padStart(15, "*")); // "***Hello, World"
console.log("a,b,c".split(",").join("-")); // "a-b-c"Template Literals
Template literals (backticks) allow string interpolation with ${}, multi-line strings, and tagged templates. They're far more readable than string concatenation. Tagged templates let you process template literals with a function — used by styled-components, graphql-tag, etc.
const name = "Alice";
const age = 30;
// Template literals (backticks)
const greeting = `Hello, ${name}! You are ${age} years old.`;
console.log(greeting);
// Multi-line strings
const html = `
<div>
<h1>${name}</h1>
<p>Age: ${age}</p>
</div>
`;
// Expressions inside ${}
console.log(`Next year: ${age + 1}`);
console.log(`Upper: ${name.toUpperCase()}`);
console.log(`${name.length} chars`);
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) =>
acc + str + (values[i] ? `<b>${values[i]}</b>` : ''), '');
}
const result = highlight`Name: ${name}, Age: ${age}`;
// "Name: <b>Alice</b>, Age: <b>30</b>"String Search & Regex
JavaScript strings support regex via match(), matchAll(), replace(), search(), and split(). Named capture groups (?<name>...) (ES2018) make regex more readable. matchAll() returns an iterator (more efficient than match() for global regex). Use .test() to check if a pattern matches without extracting.
const text = "The quick brown fox jumps over the lazy dog";
// Search methods
console.log(text.search(/brown/)); // 10 (index of match)
console.log(text.match(/\w+/g)); // ["The","quick","brown",...]
console.log(text.matchAll(/\w+/g)); // iterator of matches
// Replace with regex
console.log(text.replace(/o/g, "0")); // replace all 'o'
console.log(text.replace(/(\w+)/g, "\$1!")); // capture group
// Capture groups
const date = "2024-01-15";
const [, year, month, day] = date.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(year, month, day); // 2024 01 15
// Named capture groups (ES2018)
const m = date.match(/(?<year>\d{4})-(?<month>\d{2})/);
console.log(m.groups.year); // 2024
console.log(m.groups.month); // 01
// Test if pattern matches
const emailRe = /^[^@]+@[^@]+\.[^@]+$/;
console.log(emailRe.test("[email protected]")); // trueString Iteration & Spread
Strings are iterable with for...of. The spread operator [...] splits a string into characters — essential for correct emoji handling (surrogate pairs). String comparison is lexicographic by UTF-16 code unit, so use localeCompare() for locale-aware sorting. Emoji and some characters are 2 code units long.
const s = "Hello";
// Iterate characters
for (const char of s) {
console.log(char); // H, e, l, l, o
}
// Spread into array
const chars = [...s]; // ["H", "e", "l", "l", "o"]
console.log(chars);
// Spread with map/filter
const upper = [...s].map(c => c.toUpperCase()).join("");
console.log(upper); // HELLO
// String comparison
console.log("a" < "b"); // true (lexicographic)
console.log("apple" < "banana"); // true
console.log("Z" < "a"); // true (uppercase < lowercase in ASCII)
// Locale-aware comparison
console.log("ö".localeCompare("o", "de")); // locale-specific
// Repeat
console.log("ab".repeat(3)); // "ababab"
// Code points (handles emoji correctly)
const emoji = "😀";
console.log(emoji.length); // 2 (surrogate pair!)
console.log([...emoji].length); // 1 (correct)
console.log("😀".codePointAt(0)); // 128512Numbers & Math
Numbers & Operators
JavaScript has a single number type (64-bit float) — no separate int/float. BigInt (n suffix) handles integers beyond 2^53. Floating-point arithmetic has precision issues (0.1 + 0.2 !== 0.3) — use Number.EPSILON for comparisons. ** is the exponentiation operator (ES2016).
// JavaScript has one number type (IEEE 754 double)
const int = 42;
const float = 3.14;
const exp = 1e6; // 1000000
const hex = 0xff; // 255
const bin = 0b1010; // 10
const oct = 0o755; // 493
const big = 9007199254740993n; // BigInt (arbitrary precision)
// Arithmetic
console.log(10 / 3); // 3.333...
console.log(10 % 3); // 1 (remainder)
console.log(2 ** 10); // 1024 (exponent)
console.log(Math.floor(10 / 3)); // 3
console.log(Math.trunc(-3.7)); // -3 (toward zero)
// Increment/decrement
let x = 5;
console.log(x++); // 5 (post-increment, returns old)
console.log(++x); // 7 (pre-increment, returns new)
// Floating point issues
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false!
// Fix: use Number.EPSILON or round
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // trueMath Object
The Math object provides constants and functions. All trig functions use radians. Math.random() returns [0, 1) — multiply and floor for integer ranges. For cryptographic randomness, use crypto.getRandomValues(). Math.max/min don't accept arrays directly — spread them with ....
// Constants
console.log(Math.PI); // 3.141592653589793
console.log(Math.E); // 2.718281828459045
console.log(Math.SQRT2); // 1.4142135623730951
// Rounding
console.log(Math.round(3.7)); // 4 (nearest)
console.log(Math.floor(3.7)); // 3 (down)
console.log(Math.ceil(3.2)); // 4 (up)
console.log(Math.trunc(-3.7)); // -3 (toward zero)
console.log(Math.sign(-5)); // -1 (sign: -1, 0, or 1)
// Power & roots
console.log(Math.pow(2, 10)); // 1024
console.log(Math.sqrt(144)); // 12
console.log(Math.cbrt(27)); // 3
console.log(Math.abs(-5)); // 5
// Min/Max
console.log(Math.max(1, 5, 3)); // 5
console.log(Math.min(1, 5, 3)); // 1
console.log(Math.max(...[1, 5, 3])); // 5 (spread array)
// Trigonometry (radians)
console.log(Math.sin(Math.PI / 2)); // 1
console.log(Math.cos(0)); // 1
console.log(Math.PI / 180 * 90); // radians from degrees
// Random
console.log(Math.random()); // 0 to <1
console.log(Math.floor(Math.random() * 100)); // 0-99 integerNumber Methods & Parsing
Always specify the radix (base) for parseInt() — older browsers interpret leading zeros as octal. Number.isNaN() is reliable; global isNaN() coerces (isNaN('abc') is true). toFixed() returns a string, not a number. Numbers beyond MAX_SAFE_INTEGER lose precision — use BigInt.
// Number methods
const num = 1234.5678;
console.log(num.toFixed(2)); // "1234.57" (string)
console.log(num.toPrecision(3)); // "1.23e+3"
console.log(num.toString(2)); // binary string
console.log((255).toString(16)); // "ff" (hex)
// Number object methods
console.log(Number.isInteger(42)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isNaN(NaN)); // true (reliable)
console.log(Number.isNaN("NaN")); // false (global isNaN would be true)
console.log(Number.parseInt("42px")); // 42
console.log(Number.parseFloat("3.14")); // 3.14
// Parsing strings
console.log(parseInt("42", 10)); // 42 (always specify radix!)
console.log(parseInt("0xff", 16)); // 255
console.log(parseFloat("3.14abc")); // 3.14
// Safe integers
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.isSafeInteger(2 ** 53)); // false
// NaN checks
const result = Number("abc"); // NaN
console.log(Number.isNaN(result)); // true
console.log(result === NaN); // false! NaN !== NaNData Structures
Arrays
Arrays are dynamic, ordered, and can hold mixed types. push/pop are O(1); shift/unshift/splice are O(n). find()/findIndex() take a predicate function. forEach() doesn't return anything; use map() to transform. Arrays are objects — typeof [] is 'object'. Use Array.isArray() to check.
// Creating arrays
const nums = [1, 2, 3, 4, 5];
const mixed = [1, "hello", true, null];
const empty = new Array(5); // [empty x 5]
// Access & modify
console.log(nums[0]); // 1
console.log(nums.length); // 5
nums[0] = 0; // modify
console.log(nums.at(-1)); // 5 (negative index, ES2022)
// Add/remove
nums.push(6); // add to end, returns new length
nums.pop(); // remove from end, returns element
nums.unshift(0); // add to start
nums.shift(); // remove from start
nums.splice(1, 2); // remove 2 elements at index 1
nums.splice(1, 0, "a"); // insert at index 1
// Search
console.log(nums.indexOf(3)); // index or -1
console.log(nums.includes(3)); // true/false
console.log(nums.find(n => n > 3)); // first match
console.log(nums.findIndex(n => n > 3)); // index of first match
// Iterate
nums.forEach((val, idx) => console.log(idx, val));
for (const [idx, val] of nums.entries()) {
console.log(idx, val);
}Array Methods (map, filter, reduce)
map/filter/reduce are the holy trinity of functional array programming. map transforms, filter selects, reduce aggregates. They're chainable and don't mutate the original (except reverse/sort). sort() converts to strings by default — always provide a comparator for numbers. flat() flattens nested arrays.
const nums = [1, 2, 3, 4, 5];
// map - transform each element (returns new array)
const doubled = nums.map(n => n * 2); // [2, 4, 6, 8, 10]
const withIndex = nums.map((n, i) => `${i}:${n}`);
// filter - keep elements that pass test (returns new array)
const evens = nums.filter(n => n % 2 === 0); // [2, 4]
// reduce - accumulate to single value
const sum = nums.reduce((acc, n) => acc + n, 0); // 15
const product = nums.reduce((acc, n) => acc * n, 1); // 120
const max = nums.reduce((a, b) => Math.max(a, b));
// Chaining
const result = nums
.filter(n => n % 2 === 0) // [2, 4]
.map(n => n * 10) // [20, 40]
.reduce((sum, n) => sum + n, 0); // 60
// Other useful methods
console.log(nums.slice(1, 3)); // [2, 3] (copy portion)
console.log(nums.concat([6, 7])); // [1,2,3,4,5,6,7]
console.log([...nums, 6, 7]); // spread (modern)
console.log(nums.reverse()); // reverse in-place
console.log(nums.sort((a, b) => a - b)); // numeric sort
console.log([3, 1, 2].sort()); // [1, 2, 3] (string sort!)
console.log(nums.flat()); // flatten one level
console.log([1, [2, [3]]].flat(Infinity)); // [1, 2, 3]Objects
Objects are key-value collections (keys are strings or symbols). Dot notation for static keys, bracket notation for dynamic/special keys. Computed property names {[expr]: val} are ES6. Object.keys/values/entries extract arrays; Object.fromEntries reverses entries. for...in iterates keys (including inherited).
// Object literal
const user = {
name: "Alice",
age: 30,
"is-admin": false, // keys with special chars need quotes
greet() { // method shorthand
return `Hello, I'm ${this.name}`;
}
};
// Access
console.log(user.name); // dot notation
console.log(user["is-admin"]); // bracket notation (for dynamic/special keys)
// Computed property names (ES6)
const key = "dynamic";
const obj = { [key]: "value", [`id_${1}`]: 100 };
// Add/modify/delete
user.email = "[email protected]"; // add
user.age = 31; // modify
delete user["is-admin"]; // delete
// Check property existence
console.log("name" in user); // true
console.log(user.hasOwnProperty("name")); // true
// Iterate
for (const key in user) {
console.log(key, user[key]);
}
// Object methods
console.log(Object.keys(user)); // ["name", "age", ...]
console.log(Object.values(user)); // ["Alice", 31, ...]
console.log(Object.entries(user)); // [["name","Alice"], ...]
console.log(Object.fromEntries([["a", 1]])); // {a: 1}Destructuring & Spread
Destructuring extracts values from objects/arrays concisely. Supports renaming (key: newName), defaults (= value), and rest (...rest). Spread (...) expands iterables/objects — great for merging and shallow copying. Object spread overrides duplicate keys (last wins). Destructuring in function params is powerful for optional config.
// Object destructuring
const user = { name: "Alice", age: 30, email: "[email protected]" };
const { name, age } = user; // extract by key
const { name: fullName, email = "N/A" } = user; // rename + default
const { ...rest } = user; // rest pattern
// name -> undefined, fullName -> "Alice", rest -> {age, email}
// Array destructuring
const [a, b, c] = [1, 2, 3];
const [first, , third] = [1, 2, 3]; // skip elements
const [head, ...tail] = [1, 2, 3, 4]; // head=1, tail=[2,3,4]
// Swap variables
let x = 1, y = 2;
[x, y] = [y, x]; // x=2, y=1
// Nested destructuring
const { data: { users } } = response;
const [[a, b], [c, d]] = [[1, 2], [3, 4]];
// Function parameters
function greet({ name, greeting = "Hello" }) {
return `${greeting}, ${name}`;
}
greet({ name: "Alice" }); // "Hello, Alice"
// Spread operator
const arr1 = [1, 2], arr2 = [3, 4];
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
const obj1 = { a: 1 }, obj2 = { b: 2 };
const combined = { ...obj1, ...obj2 }; // {a:1, b:2}
const clone = { ...user }; // shallow copyMap, Set, WeakMap
Map allows any key type (objects, not just strings) and maintains insertion order — unlike plain objects. Set stores unique values — perfect for deduplication. WeakMap/WeakSet keys are weakly referenced (can be garbage collected), preventing memory leaks. Use Map when you need non-string keys or frequent add/delete.
// Map - key-value pairs, any key type, maintains insertion order
const map = new Map();
map.set("name", "Alice");
map.set(42, "number key");
map.set({ obj: true }, "object key");
console.log(map.get("name")); // "Alice"
console.log(map.has("name")); // true
console.log(map.size); // 3
map.delete(42);
map.clear();
// Iterate Map
for (const [key, value] of map) {
console.log(key, value);
}
// Set - unique values
const set = new Set([1, 2, 3, 2, 1]);
console.log(set.size); // 3 (duplicates removed)
set.add(4);
set.has(2); // true
set.delete(1);
// Set operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]); // {1,2,3,4}
const intersection = new Set([...a].filter(x => b.has(x))); // {2,3}
const difference = new Set([...a].filter(x => !b.has(x))); // {1}
// WeakMap/WeakSet - keys must be objects, GC-friendly
const weakMap = new WeakMap();
weakMap.set({}, "value"); // key can be garbage collectedControl Flow
If / Else & Ternary
Use if/else for complex logic; ternary for simple value selection. && and || short-circuit (useful for defaults/conditionals). ?? (nullish coalescing) only checks null/undefined, unlike || which checks all falsy values. ?. (optional chaining) safely accesses nested properties without errors.
const score = 85;
// if / else if / else
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else {
console.log("C");
}
// Ternary operator (expression)
const grade = score >= 60 ? "pass" : "fail";
// Nested ternary (avoid - hard to read)
const status = score >= 90 ? "excellent"
: score >= 80 ? "good"
: score >= 60 ? "pass"
: "fail";
// Short-circuit evaluation
const name = user?.name || "Anonymous"; // default value
const value = condition && doSomething(); // execute if true
// Nullish coalescing (??) - only null/undefined, not falsy
const count = 0 ?? 10; // 0 (not 10, because 0 is not null/undefined)
const name2 = null ?? "default"; // "default"
// Optional chaining (?.)
const city = user?.address?.city; // undefined if any link is null
const length = user?.name?.length; // undefined if user or name is nullSwitch Statement
switch compares with strict equality (===), so type matters. Don't forget break — without it, execution falls through to the next case. Group cases by stacking them (case 6: case 7:). Switch is cleaner than long if/else chains for discrete values. Modern code sometimes prefers object lookup tables.
const day = 3;
// Traditional switch (use break!)
switch (day) {
case 1:
console.log("Monday");
break; // without break, falls through!
case 2:
console.log("Tuesday");
break;
case 6:
case 7: // multiple cases share code
console.log("Weekend");
break;
default:
console.log("Weekday");
}
// Switch with return (no break needed)
function getColor(type) {
switch (type) {
case "success": return "green";
case "error": return "red";
case "warning": return "yellow";
default: return "gray";
}
}
// Strict equality (===)
switch (1) {
case "1": console.log("string"); // NOT matched
case 1: console.log("number"); // matched
}Loops
Use for...of for arrays/strings (values), for...in for objects (keys) — never for...in on arrays (it iterates indices as strings and includes prototype). while checks before running; do...while runs at least once. break exits, continue skips. Use .entries() to get index+value with for...of.
// for loop
for (let i = 0; i < 5; i++) {
console.log(i);
}
// for...of (iterable values - arrays, strings, maps)
const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
console.log(fruit);
}
// for...in (object keys - AVOID for arrays!)
const user = { name: "Alice", age: 30 };
for (const key in user) {
console.log(key, user[key]);
}
// while
let count = 0;
while (count < 3) {
console.log(count);
count++;
}
// do...while (runs at least once)
let i = 0;
do {
console.log(i);
i++;
} while (i < 3);
// break & continue
for (let i = 0; i < 10; i++) {
if (i === 5) break; // exit loop
if (i % 2 === 0) continue; // skip iteration
console.log(i);
}
// Iterate with index
for (const [index, value] of fruits.entries()) {
console.log(index, value);
}Iterators & Generators
Iterators implement next() returning {value, done}. Generators (function*) simplify iterator creation with yield — they pause execution and resume on next(). Generators are lazy (compute on demand) and can be infinite. Use them for custom iterables, sequences, and async flows.
// Iterable protocol (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 }
: { done: true };
}
};
}
};
for (const num of range) console.log(num); // 1, 2, 3, 4, 5
console.log([...range]); // [1, 2, 3, 4, 5]
// Generator function (function*)
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const gen = idGenerator();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: 2, done: false}
console.log(gen.next().value); // 3
// Generator with yield*
function* nested() {
yield 1;
yield* [2, 3, 4]; // delegate to another iterable
yield 5;
}Functions
Function Declarations & Expressions
Function declarations are hoisted (can be called before definition); expressions are not. Arrow functions are concise and don't have their own 'this' (they inherit from enclosing scope). IIFEs create private scopes (less needed with modules). Functions are first-class — pass them as arguments, return them, store in variables.
// Function declaration (hoisted - can be called before definition)
greet("Alice"); // works (hoisted)
function greet(name) {
return `Hello, ${name}!`;
}
// Function expression (not hoisted)
const greet2 = function(name) {
return `Hi, ${name}!`;
};
// Named function expression (for recursion/stack traces)
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1);
};
// Arrow function (ES6) - concise, no own this
const add = (a, b) => a + b;
const square = x => x * x;
const greet3 = () => "Hello!";
const log = x => { console.log(x); }; // block body needs return
// IIFE (Immediately Invoked Function Expression)
const result = (function() {
const private = "secret";
return private.toUpperCase();
})();
// Functions are first-class (can be passed/returned)
function apply(fn, value) {
return fn(value);
}
console.log(apply(square, 5)); // 25Arrow Functions & this
Arrow functions don't have their own 'this', 'arguments', 'super', or 'new.target' — they inherit from the enclosing scope. This makes them perfect for callbacks (especially in class methods). But they can't be used as constructors or methods that need their own 'this'. Use regular functions for object methods.
// Arrow function variations
const add = (a, b) => a + b; // implicit return
const greet = name => `Hi ${name}`; // single param, no parens
const log = () => console.log("hi"); // no params
const obj = (x, y) => ({ x, y }); // return object needs parens
const multi = (a, b) => { // block body
const sum = a + b;
return sum * 2;
};
// Arrow functions don't have their own 'this'
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // 'this' refers to Timer instance
console.log(this.seconds);
}, 1000);
}
// Regular function has its own 'this'
function Counter() {
this.count = 0;
// Regular function: 'this' is undefined (strict) or global
document.addEventListener("click", function() {
// this.count++; // ERROR: this is not Counter
});
// Arrow function: 'this' is Counter
document.addEventListener("click", () => {
this.count++; // works!
});
}
// Arrow functions can't be constructors
// const obj = new arrowFunc(); // TypeErrorClosures
Closures are functions that 'remember' the variables from their defining scope, even after that scope exits. They enable data privacy (module pattern), memoization, currying, and partial application. Every function in JavaScript is a closure. The inner function keeps a reference to outer variables, not a copy.
// A closure is a function that remembers its outer variables
function makeCounter() {
let count = 0; // private variable
return function() {
return ++count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count is private - can't access directly
// Module pattern (pre-ES6)
const calculator = (function() {
const result = 0; // private
return {
add(x) { return result + x; },
multiply(x) { return result * x; }
};
})();
// Practical: memoization
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (!(key in cache)) {
cache[key] = fn.apply(this, args);
}
return cache[key];
};
}
const slowFib = memoize(n =>
n < 2 ? n : slowFib(n - 1) + slowFib(n - 2)
);
// Currying with closures
const multiply = a => b => a * b;
const double = multiply(2);
console.log(double(5)); // 10Arguments & Rest/Spread
Default parameters provide fallback values. Rest parameters (...name) collect extra arguments into a real array — prefer over the legacy 'arguments' object. Spread (...) expands an array into individual arguments. Destructuring in parameters enables named, optional config objects — a common API pattern.
// Default parameters
function greet(name = "Guest", greeting = "Hello") {
return `${greeting}, ${name}!`;
}
greet(); // "Hello, Guest!"
greet("Alice"); // "Hello, Alice!"
greet("Bob", "Hi"); // "Hi, Bob!"
// Rest parameters (...args collects into array)
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
function log(tag, ...args) {
console.log(tag, args);
}
// Spread (opposite of rest)
const nums = [1, 2, 3];
console.log(Math.max(...nums)); // 3 (spread array as args)
console.log(sum(...nums)); // 6
// arguments object (old way, avoid in modern code)
function old() {
console.log(arguments); // array-like, not a real array
const args = Array.from(arguments); // convert
}
// Destructuring parameters
function process({ name, age = 0 } = {}) {
console.log(name, age);
}
process({ name: "Alice" }); // Alice 0
process(); // undefined 0 (default empty object)OOP & Classes
Classes & Constructor
ES6 classes are syntactic sugar over prototypes. Private fields (#name) are ES2022 and truly private (unlike _name convention). Getters/setters allow computed properties. Static members belong to the class, not instances. Class fields (name = value) initialize instance properties without constructor.
class Person {
// Fields (class fields proposal - ES2022)
species = "human"; // instance field
// Private fields (ES2022)
#ssn = "secret"; // truly private
// Static field/method
static count = 0;
// Constructor
constructor(name, age) {
this.name = name;
this.age = age;
Person.count++;
}
// Instance method
greet() {
return `Hello, I'm ${this.name}`;
}
// Getter
get info() {
return `${this.name}, ${this.age}`;
}
// Setter
set age(value) {
if (value < 0) throw new Error("Invalid age");
this._age = value;
}
get age() {
return this._age;
}
// Static method
static create(name) {
return new Person(name, 0);
}
}
const alice = new Person("Alice", 30);
console.log(alice.greet()); // Hello, I'm Alice
console.log(alice.info); // Alice, 30
console.log(Person.count); // 1
// alice.#ssn; // SyntaxError - private!Inheritance & Polymorphism
extends creates inheritance; super() calls the parent constructor (required before using 'this'). Override methods by redefining them. JavaScript is single-inheritance, but mixins (class factories) provide composition. instanceof checks the prototype chain. Polymorphism works through method overriding.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
// Static method can be inherited
static create(type, name) {
return new type(name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // must call super() first
this.breed = breed;
}
speak() {
return `${this.name} barks`; // override
}
fetch() {
return `${this.name} fetches`;
}
}
class Cat extends Animal {
speak() {
return `${this.name} meows`;
}
}
// Polymorphism
const animals = [new Dog("Rex", "Lab"), new Cat("Whiskers")];
animals.forEach(a => console.log(a.speak()));
// Rex barks
// Whiskers meows
// instanceof checks
const dog = new Dog("Buddy", "Poodle");
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog.constructor.name); // "Dog"
// Mixins (multiple inheritance alternative)
const Walker = (Base) => class extends Base {
walk() { return `${this.name} walks`; }
};
class Robot extends Walker(Animal) {}Prototypes
JavaScript uses prototypal inheritance — objects inherit from other objects via a prototype chain. __proto__ is deprecated; use Object.getPrototypeOf/setPrototypeOf. Classes are syntactic sugar over this system. Modifying built-in prototypes (Array.prototype) is dangerous — it can break code. Prefer composition over deep inheritance.
// Every object has a prototype (chain of inheritance)
const obj = {};
console.log(obj.__proto__); // Object.prototype
console.log(Object.getPrototypeOf(obj)); // preferred
// Constructor function (pre-class syntax)
function OldPerson(name) {
this.name = name;
}
OldPerson.prototype.greet = function() {
return `Hi, ${this.name}`;
};
const p = new OldPerson("Alice");
console.log(p.greet()); // Hi, Alice
// Prototype chain
// p -> OldPerson.prototype -> Object.prototype -> null
// Adding to prototype (affects all instances)
Array.prototype.last = function() {
return this[this.length - 1];
};
console.log([1, 2, 3].last()); // 3
// Object.create (prototypal inheritance)
const animal = { type: "unknown" };
const dog = Object.create(animal);
dog.type = "dog";
console.log(dog.type); // "dog" (own property)
// Check own vs inherited
console.log(dog.hasOwnProperty("type")); // true
console.log("type" in dog); // true (includes inherited)
// Get prototype chain
let proto = Object.getPrototypeOf(dog);
while (proto) {
console.log(proto);
proto = Object.getPrototypeOf(proto);
}Error Handling
Try / Catch / Finally
try/catch/finally handles exceptions. catch binds the error object (which has .message and .stack). Use instanceof to handle specific error types differently. Always re-throw unknown errors after handling expected ones. Create custom errors by extending Error for application-specific error handling.
try {
const result = JSON.parse('{"invalid json"');
} catch (error) {
console.error("Parse error:", error.message);
console.error("Stack:", error.stack);
} finally {
console.log("Always runs");
}
// Catch specific error types
try {
const data = JSON.parse(input);
if (!data.name) throw new TypeError("name is required");
} catch (error) {
if (error instanceof SyntaxError) {
console.log("JSON syntax error");
} else if (error instanceof TypeError) {
console.log("Type error:", error.message);
} else {
throw error; // re-throw unknown errors
}
}
// Error properties
const err = new Error("Something went wrong");
err.code = "CUSTOM_ERROR";
err.statusCode = 500;
throw err;
// Built-in error types
// Error, TypeError, RangeError, ReferenceError
// SyntaxError, URIError, EvalErrorCustom Errors
Extend Error to create custom error types with extra context (fields, codes). Always set this.name to match the class name. Use instanceof to catch specific error types. Error chaining (ES2022 { cause }) preserves the original error for debugging. A good error hierarchy makes error handling precise.
// Custom error class
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = "DatabaseError";
this.query = query;
}
}
// Usage
function validateUser(user) {
if (!user.email) {
throw new ValidationError("email", "Email is required");
}
if (!user.email.includes("@")) {
throw new ValidationError("email", "Invalid email format");
}
}
// Handling custom errors
try {
validateUser({ name: "Alice" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed: ${error.field} - ${error.message}`);
} else if (error instanceof DatabaseError) {
console.log(`DB error in query: ${error.query}`);
} else {
console.log("Unexpected error:", error);
}
}
// Error chaining (ES2022)
try {
throw new Error("Original cause");
} catch (cause) {
throw new Error("Failed to process", { cause });
}Async & Promises
Callbacks
Callbacks are functions passed to be called later. The error-first convention (err, data) is standard in Node.js. Nested callbacks create 'callback hell' — deeply nested, hard to read code. Promises and async/await solve this. setTimeout/setInterval are common callback-based APIs.
// Callback pattern (old way)
function fetchData(url, callback) {
setTimeout(() => {
callback(null, { data: "result" });
}, 1000);
}
// Callback with error-first convention (Node.js style)
fetchData("/api", (error, data) => {
if (error) {
console.error(error);
return;
}
console.log(data);
});
// Callback hell (pyramid of doom)
fetchUser(userId, (err, user) => {
if (err) return handleError(err);
fetchPosts(user.id, (err, posts) => {
if (err) return handleError(err);
fetchComments(posts[0].id, (err, comments) => {
if (err) return handleError(err);
// deeply nested...
});
});
});
// Event listeners are callbacks
button.addEventListener("click", (event) => {
console.log("Clicked!", event.target);
});
// setTimeout / setInterval
setTimeout(() => console.log("After 1s"), 1000);
const id = setInterval(() => console.log("tick"), 1000);
clearInterval(id); // stopPromises
Promises represent future values with three states: pending, fulfilled, rejected. .then() handles success, .catch() handles errors, .finally() always runs. Promise.all() waits for all (fails fast); allSettled() waits for all (never fails); race() returns first settled; any() returns first successful.
// Creating a promise
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Data received");
} else {
reject(new Error("Failed"));
}
});
// Consuming a promise
promise
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log("Done"));
// Chaining
fetch("/api/users")
.then(response => response.json())
.then(users => users.filter(u => u.active))
.then(active => console.log(active))
.catch(err => console.error("Error:", err));
// Promise.all - wait for all (fails if any fails)
Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json())
]).then(([users, posts]) => {
console.log(users, posts);
});
// Promise.allSettled - wait for all (never fails)
Promise.allSettled([p1, p2]).then(results => {
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.log(r.reason);
});
});
// Promise.race - first to settle wins
Promise.race([p1, p2]).then(first => console.log(first));
// Promise.any - first to succeed
Promise.any([p1, p2]).then(first => console.log(first));Async / Await
async/await is syntactic sugar over promises — makes async code look synchronous. 'await' pauses the function until the promise settles. Always wrap await in try/catch for error handling. Use Promise.all() for parallel operations (faster than sequential await in a loop). Top-level await works in ES modules.
// async function always returns a Promise
async function fetchData() {
// await pauses until the promise resolves
const response = await fetch("/api/users");
const data = await response.json();
return data;
}
// Error handling with try/catch
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
} catch (error) {
console.error("Failed:", error);
return null;
}
}
// Sequential vs parallel
async function sequential() {
const a = await fetch("/api/a"); // waits for a
const b = await fetch("/api/b"); // then waits for b
return [a, b];
}
async function parallel() {
const [a, b] = await Promise.all([ // both at once
fetch("/api/a"),
fetch("/api/b")
]);
return [a, b];
}
// Top-level await (ES2022, in modules)
// const data = await fetch("/api").then(r => r.json());
// Iterating with async
async function processUrls(urls) {
for (const url of urls) {
const data = await fetch(url);
console.log(data);
}
}
// Promise.all with map (parallel)
async function fetchAll(urls) {
return Promise.all(urls.map(url => fetch(url)));
}DOM Manipulation
Selecting & Modifying Elements
querySelector/querySelectorAll (CSS selectors) are the modern way to select elements. textContent is safer than innerHTML (prevents XSS). classList provides add/remove/toggle/contains for classes. dataset accesses data-* attributes. Always sanitize user input before setting innerHTML.
// Selecting elements
const el = document.querySelector("#app"); // first match
const all = document.querySelectorAll(".item"); // all matches (NodeList)
const byId = document.getElementById("app");
const byClass = document.getElementsByClassName("item"); // HTMLCollection
// Modifying content
el.textContent = "Hello"; // text only (safe from XSS)
el.innerHTML = "<b>Bold</b>"; // HTML (XSS risk with user input!)
el.innerText = "Visible text"; // respects CSS visibility
// Attributes
el.setAttribute("data-id", "123");
const id = el.getAttribute("data-id");
el.removeAttribute("disabled");
el.dataset.id; // access data-* attributes
el.id = "new-id";
el.className = "active highlighted";
el.classList.add("active");
el.classList.remove("old");
el.classList.toggle("hidden");
el.classList.contains("active");
// Styles
el.style.color = "red";
el.style.backgroundColor = "blue"; // camelCase
el.style.cssText = "color: red; font-size: 16px;";
// Creating elements
const div = document.createElement("div");
div.textContent = "New element";
div.classList.add("box");
document.body.appendChild(div);
document.body.prepend(div); // add to beginning
document.body.insertBefore(div, referenceEl);Events
addEventListener is preferred over on<event> properties (allows multiple listeners). Event delegation (listening on a parent) is efficient for dynamically added elements. e.target is what was clicked; e.currentTarget is the element with the listener. preventDefault() stops default behavior; stopPropagation() stops bubbling.
// Add event listener
button.addEventListener("click", (event) => {
console.log("Clicked!", event.target);
console.log("Current target:", event.currentTarget);
});
// Common events
// click, dblclick, mousedown, mouseup, mousemove
// keydown, keyup, keypress
// submit, change, input, focus, blur
// load, DOMContentLoaded, resize, scroll
// Event object properties
input.addEventListener("keydown", (e) => {
console.log(e.key); // "Enter", "a", etc.
console.log(e.code); // "KeyA", "Enter"
console.log(e.ctrlKey); // true if Ctrl held
e.preventDefault(); // stop default behavior
e.stopPropagation(); // stop bubbling
});
// Event delegation (efficient for many elements)
document.addEventListener("click", (e) => {
if (e.target.matches(".delete-btn")) {
const id = e.target.dataset.id;
deleteItem(id);
}
});
// Custom events
const customEvent = new CustomEvent("userLogin", {
detail: { userId: 123 }
});
element.dispatchEvent(customEvent);
element.addEventListener("userLogin", (e) => {
console.log("User logged in:", e.detail.userId);
});
// Remove listener (must be same function reference)
const handler = () => console.log("click");
button.addEventListener("click", handler);
button.removeEventListener("click", handler);Modules & JSON
ES Modules
ES Modules (import/export) are the modern standard, supported in browsers and Node.js. Default export (one per module) vs named exports (multiple). Dynamic import() enables lazy loading. Modules are always in strict mode and have their own scope. Use type='module' in HTML script tags.
// math.js - exporting
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Calculator { /* ... */ }
// Default export (one per module)
export default function greet(name) {
return `Hello, ${name}`;
}
// main.js - importing
import greet from "./math.js"; // default
import { add, PI } from "./math.js"; // named
import * as math from "./math.js"; // namespace
import { add as plus } from "./math.js"; // rename
import greet, { add } from "./math.js"; // default + named
// Dynamic import (returns Promise)
const module = await import("./math.js");
console.log(module.add(2, 3));
// Conditional/dynamic loading
if (featureEnabled) {
const { default: Feature } = await import("./feature.js");
new Feature();
}
// Re-export
export { add } from "./math.js";
export * from "./utils.js";
// In HTML
// <script type="module" src="app.js"></script>
// Modules are deferred and in strict mode by defaultJSON
JSON.stringify() converts JS values to JSON strings; JSON.parse() reverses it. Use replacers/revivers to filter or transform during conversion. JSON doesn't support functions, undefined, Dates (they become strings), or circular references. fetch().json() parses JSON responses automatically. Always wrap JSON.parse in try/catch for untrusted input.
// JSON is the standard data interchange format
// JSON supports: string, number, boolean, null, array, object
// JavaScript object to JSON string
const user = { name: "Alice", age: 30, active: true };
const jsonStr = JSON.stringify(user);
// '{"name":"Alice","age":30,"active":true}'
// Pretty print
const pretty = JSON.stringify(user, null, 2);
// {
// "name": "Alice",
// "age": 30,
// "active": true
// }
// JSON string to JavaScript object
const parsed = JSON.parse('{"name":"Bob","age":25}');
console.log(parsed.name); // "Bob"
// Replacer function (filter/transform during stringify)
const filtered = JSON.stringify(user, (key, value) => {
if (key === "age") return undefined; // exclude
return value;
});
// Reviver function (transform during parse)
const data = JSON.parse(jsonStr, (key, value) => {
if (key === "date") return new Date(value);
return value;
});
// Fetch JSON from API
async function getUsers() {
const res = await fetch("/api/users");
return res.json(); // parses JSON automatically
}
// JSON limitations
// - No functions, undefined, or dates
// - No circular references (throws error)
// - Keys must be strings (with quotes)Date & Time
JavaScript Date is notoriously awkward — months are 0-indexed (January = 0), days are 1-indexed. Date objects are mutable. toISOString() gives UTC; toLocaleString() gives local time. For serious date work, use a library like date-fns or dayjs. Intl.DateTimeFormat provides locale-aware formatting.
// Create dates
const now = new Date();
const specific = new Date("2024-01-15T10:30:00");
const fromMs = new Date(1705315200000);
const fromParts = new Date(2024, 0, 15, 10, 30); // month is 0-indexed!
// Get components
console.log(now.getFullYear()); // 2024
console.log(now.getMonth()); // 0-11 (January = 0!)
console.log(now.getDate()); // 1-31
console.log(now.getDay()); // 0-6 (Sunday = 0)
console.log(now.getHours()); // 0-23
console.log(now.getTime()); // milliseconds since epoch
// Set components
now.setFullYear(2025);
now.setMonth(11); // December
// Formatting
console.log(now.toISOString()); // "2024-01-15T10:30:00.000Z"
console.log(now.toLocaleDateString()); // "1/15/2024" (locale)
console.log(now.toLocaleString()); // "1/15/2024, 10:30:00 AM"
// Timestamps
const start = Date.now(); // milliseconds since epoch
// ... do work ...
console.log(`Took ${Date.now() - start}ms`);
// Intl for formatting (modern)
const formatter = new Intl.DateTimeFormat("zh-CN", {
year: "numeric", month: "long", day: "numeric"
});
console.log(formatter.format(now)); // "2024年1月15日"ES6+ Features
let, const & Block Scoping
Prefer const by default, let when reassignment is needed, and avoid var entirely. const prevents reassignment but objects/arrays are still mutable. let and const are block-scoped and live in the Temporal Dead Zone before declaration (unlike var which is hoisted as undefined). This prevents many subtle bugs.
// var is function-scoped (hoisted, leaks out of blocks)
// let and const are block-scoped (stay inside {})
{
var x = 1; // accessible outside the block
let y = 2; // block-scoped
const z = 3; // block-scoped, cannot reassign
}
console.log(x); // 1
// console.log(y); // ReferenceError
// const prevents reassignment, NOT mutation
const arr = [1, 2, 3];
arr.push(4); // OK — mutating the array
// arr = [5]; // TypeError — reassigning const
// Temporal Dead Zone: let/const can't be used before declaration
// console.log(a); // ReferenceError (not undefined like var)
let a = 10;Arrow Functions & this
Arrow functions are concise and inherit 'this' from the enclosing scope — perfect for callbacks and methods that need the outer 'this'. But they can't be used as constructors and have no 'arguments' object. Don't use arrow functions for object methods if you need 'this' to refer to the object (use regular methods instead).
// Arrow functions: concise syntax, lexically bound 'this'
const add = (a, b) => a + b;
const square = x => x * x; // single param, no parens
const greet = name => `Hello, ${name}`; // template literal
const noop = () => {}; // no params, empty body
// Returning an object literal needs parens
const makeUser = (name, age) => ({ name, age });
// Arrow functions DON'T have their own 'this' — they inherit it
function Counter() {
this.count = 0;
setInterval(() => {
this.count++; // 'this' is the Counter instance (lexical)
console.log(this.count);
}, 1000);
}
// Regular function would lose 'this' (it'd be window/undefined)Destructuring Assignment
Destructuring extracts values from objects/arrays into variables in one line — cleaner than manual property access. Object destructuring uses { key }, array destructuring uses [index]. Support renaming (key: alias), defaults (key = default), rest (...rest), and nested patterns. Heavily used in React props and function params.
// Object destructuring
const user = { name: "Alice", age: 30, city: "NYC" };
const { name, age } = user;
console.log(name, age); // Alice 30
// Rename and default values
const { name: fullName, country = "USA" } = user;
console.log(fullName, country); // Alice USA
// Array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first, second, rest); // 1 2 [3,4,5]
// Swap variables (no temp needed!)
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// Destructuring in function parameters
function greet({ name, greeting = "Hello" }) {
console.log(`${greeting}, ${name}`);
}
greet({ name: "Bob" }); // Hello, Bob
// Nested destructuring
const { data: { results } } = response;Spread & Rest Operators
The ... operator is 'spread' when expanding (in arrays/objects/calls) and 'rest' when collecting (in params/destructuring). Spread creates shallow copies and merges objects (later keys override earlier). Rest params replace the old 'arguments' object and are real Arrays. Both are essential modern JS idioms.
// Spread (...) expands iterables into individual elements
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5] (copy + append)
const merged = [...arr1, ...arr2];
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 } (shallow copy)
const updated = { ...obj1, b: 99 }; // override b: { a: 1, b: 99 }
// Rest (...) collects multiple elements into an array
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
// Rest in destructuring
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1 [2, 3, 4]
// Spread in function calls
Math.max(...[1, 5, 3]); // 5 (same as Math.max(1, 5, 3))Template Literals & Tagged Templates
Template literals (backticks) support multi-line strings and ${} interpolation — far cleaner than string concatenation. Tagged templates let a function process the literal parts and interpolated values, enabling custom formatting, sanitization (e.g., escaping HTML), or i18n. Popular in styled-components and graphql-tag.
// Template literals: backticks, multi-line, interpolation
const name = "Alice";
const msg = `Hello ${name},
this spans
multiple lines`;
// Expressions inside ${}
const price = 19.99;
const tax = 0.08;
console.log(`Total: ${(price * (1 + tax)).toFixed(2)}`); // Total: 21.59
// Nested template literals
const items = ["apple", "banana"];
const html = `
<ul>
${items.map(i => `<li>${i}</li>`).join("")}
</ul>
`;
// Tagged templates: function processes the literal
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) =>
acc + str + (values[i] ? `**${values[i]}**` : ""), "");
}
const result = highlight`Name: ${name}, Age: ${30}`;
// "Name: **Alice**, Age: **30**"Optional Chaining & Nullish Coalescing
Optional chaining (?.) short-circuits to undefined if any part of the chain is null/undefined — eliminates verbose manual checks. Nullish coalescing (??) provides defaults ONLY for null/undefined, unlike || which also overrides 0, '', and false. Together they handle the most common 'missing data' patterns safely. Available since ES2020.
// Optional chaining (?.): safely access nested properties
const user = { profile: { name: "Alice" } };
console.log(user?.profile?.name); // "Alice"
console.log(user?.settings?.theme); // undefined (no error!)
console.log(user?.profile?.address?.city); // undefined
// Without ?., you'd write:
// user && user.profile && user.profile.name
// Optional method calls
const result = obj?.method?.();
const firstChar = str?.[0];
// Nullish coalescing (??): default only for null/undefined
const theme = user.settings?.theme ?? "light"; // "light"
const count = 0 ?? 10; // 0 (NOT 10 — ?? keeps falsy but valid values)
const name = "" ?? "Anonymous"; // "" (empty string is kept!)
// vs || which treats all falsy values (0, "", false) as missing
const count2 = 0 || 10; // 10 (probably not what you want!)Events & Event Handling
addEventListener Basics
addEventListener is the modern way to bind events — it allows multiple handlers, supports event delegation, and offers options like once, passive, and capture. Always keep a reference to the handler if you need to remove it later (anonymous functions can't be removed). The event object carries target, currentTarget, preventDefault(), and stopPropagation().
// Modern event binding (preferred over onclick=)
const button = document.querySelector("#myBtn");
button.addEventListener("click", function(event) {
console.log("Clicked!", event.target);
event.preventDefault(); // stop default action (e.g., form submit)
});
// Multiple listeners can be attached
button.addEventListener("click", handler1);
button.addEventListener("click", handler2);
// Remove a specific listener (must be same reference)
button.removeEventListener("click", handler1);
// Common event types
// "click", "dblclick", "mousedown", "mouseup", "mousemove"
// "keydown", "keyup", "keypress"
// "submit", "change", "input", "focus", "blur"
// "load", "DOMContentLoaded", "resize", "scroll"
// Once option: auto-remove after first trigger
button.addEventListener("click", handler, { once: true });Event Delegation
Event delegation attaches one listener to a parent that handles events from all children via event bubbling. Use event.target.matches(selector) to filter. This is far more efficient than binding to each child and automatically handles dynamically added elements. The trade-off: the parent must be a common ancestor that always exists.
// Delegate events to a parent instead of each child
// Efficient for dynamically added elements
const list = document.querySelector("#item-list");
list.addEventListener("click", (event) => {
// event.target is the actual element clicked
if (event.target.matches("li.item")) {
console.log("Clicked:", event.target.textContent);
event.target.classList.toggle("selected");
}
});
// Now dynamically added items automatically work
list.insertAdjacentHTML("beforeend", "<li class='item'>New Item</li>");
// Benefits:
// 1. One listener instead of many (memory efficient)
// 2. Works for elements added after binding
// 3. No need to re-bind when DOM changesEvent Propagation (bubbling & capturing)
Events propagate in three phases: capturing (top-down), target, and bubbling (bottom-up, default). Most handlers run in the bubbling phase. stopPropagation() prevents the event from reaching parent elements; stopImmediatePropagation() also stops other handlers on the same element. Use capturing (third arg true) for handlers that must run before child handlers.
<div id="outer">
<div id="inner">
<button id="btn">Click</button>
</div>
</div>
// Events flow in three phases:
// 1. Capturing: top -> target (window -> document -> ... -> target)
// 2. Target: at the target element
// 3. Bubbling: target -> top (default phase most handlers run in)
// Bubbling (default): child fires first, then parents
btn.addEventListener("click", () => console.log("button"));
inner.addEventListener("click", () => console.log("inner"));
outer.addEventListener("click", () => console.log("outer"));
// Click button logs: button -> inner -> outer
// stopPropagation: prevent bubbling to parents
btn.addEventListener("click", (e) => {
e.stopPropagation();
console.log("only button");
});
// Capturing phase (third arg = true)
outer.addEventListener("click", handler, true); // runs during capture
// stopImmediatePropagation: stop other handlers on SAME element tooCustom Events
CustomEvent lets you create application-specific events with payload data in the 'detail' property. Combined with dispatchEvent, this enables a pub/sub pattern for decoupling components — modules communicate without direct references. Use a naming convention like 'namespace:action' to avoid collisions. This is the foundation of many custom element frameworks.
// Create and dispatch custom events
const event = new CustomEvent("userLoggedIn", {
detail: { userId: 42, name: "Alice" },
bubbles: true, // allow bubbling
});
document.dispatchEvent(event);
// Listen for the custom event
document.addEventListener("userLoggedIn", (e) => {
console.log("User logged in:", e.detail.userId, e.detail.name);
});
// Practical: decoupled communication between components
class Cart {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
window.dispatchEvent(new CustomEvent("cart:updated", {
detail: { count: this.items.length }
}));
}
}
// Any module can listen without knowing about Cart internals
window.addEventListener("cart:updated", (e) => {
updateBadge(e.detail.count);
});Keyboard & Form Events
Keyboard events give e.key (logical key like 'a', 'Enter') and e.code (physical key like 'KeyA'). Use e.key for most logic. Form submit always needs preventDefault() to stop page reload. FormData + Object.fromEntries easily collects form data. 'input' fires continuously; 'change' fires when the field loses focus — choose based on when you want validation.
// Keyboard events
document.addEventListener("keydown", (e) => {
console.log(e.key, e.code); // e.key="a", e.code="KeyA"
if (e.key === "Escape") closeModal();
if (e.ctrlKey && e.key === "s") { e.preventDefault(); save(); }
});
// Form events
const form = document.querySelector("#myForm");
form.addEventListener("submit", (e) => {
e.preventDefault(); // stop page reload
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log(data); // { username: "...", email: "..." }
});
// Input validation on change/blur
const emailInput = document.querySelector("#email");
emailInput.addEventListener("blur", () => {
if (!emailInput.value.includes("@")) {
emailInput.setCustomValidity("Enter a valid email");
} else {
emailInput.setCustomValidity("");
}
});
// Change vs input:
// 'input' fires on every keystroke; 'change' fires on blurFetch API & AJAX
Basic fetch (GET)
fetch() is the modern replacement for XMLHttpRequest — promise-based and cleaner. Crucially, fetch only rejects on network errors, NOT on HTTP error statuses (404, 500). Always check response.ok (status 200-299) before parsing. Use async/await for readable sequential code. response.json() is async because it reads the body stream.
// fetch() returns a Promise; .json() returns another Promise
fetch("https://api.example.com/users")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json(); // or .text(), .blob(), .arrayBuffer()
})
.then(data => console.log(data))
.catch(error => console.error("Fetch failed:", error));
// fetch does NOT reject on HTTP errors (404, 500) — only on network
// failures. You MUST check response.ok manually.
// Same with async/await (preferred)
async function getUsers() {
try {
const res = await fetch("https://api.example.com/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error(err);
}
}POST, PUT, DELETE with fetch
The fetch options object configures method, headers, and body. For JSON, set Content-Type: application/json and JSON.stringify the body. For file uploads, use FormData (don't set Content-Type manually — the browser adds the multipart boundary). PUT replaces a resource entirely; PATCH partially updates it.
// POST: create a resource
async function createUser(data) {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return res.json();
}
createUser({ name: "Alice", email: "[email protected]" });
// PUT: update a resource (full replace)
await fetch("/api/users/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice Updated" }),
});
// DELETE
await fetch("/api/users/1", { method: "DELETE" });
// Sending form data (file uploads)
const formData = new FormData();
formData.append("file", fileInput.files[0]);
await fetch("/upload", { method: "POST", body: formData });
// Don't set Content-Type for FormData — browser sets it with boundaryRequest Headers & Auth
Headers carry metadata and auth tokens. Bearer tokens (JWT) go in the Authorization header. Some headers are 'forbidden' (browser-controlled) like Host and Cookie. CORS is enforced by the browser, not the server — you can't bypass it from client JS; the server must send Access-Control-Allow-Origin. Preflight OPTIONS requests happen for non-simple requests.
// Custom headers (e.g., Bearer token auth)
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem("authToken");
const res = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"Accept": "application/json",
...options.headers, // allow overrides
},
});
return res;
}
// Common headers:
// Content-Type: application/json | application/x-www-form-urlencoded
// Authorization: Bearer <token> | Basic <base64>
// Accept: application/json
// X-Requested-With: XMLHttpRequest (anti-CSRF)
// CORS: browser blocks cross-origin requests unless the server
// returns Access-Control-Allow-Origin. fetch can't bypass CORS.AbortController (Cancel Requests)
AbortController cancels fetch requests — essential for search-as-you-type, navigation away, or timeouts. Pass signal to fetch; calling controller.abort() triggers an AbortError. Without this, stale requests can update the UI out of order. AbortController also works with other async APIs and is the standard cancellation mechanism in modern JS.
// AbortController lets you cancel in-flight fetch requests
const controller = new AbortController();
async function fetchWithTimeout(url, ms = 5000) {
const timeout = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} catch (err) {
if (err.name === "AbortError") {
console.log("Request was aborted (timeout or cancel)");
} else {
throw err;
}
} finally {
clearTimeout(timeout);
}
}
// Cancel manually (e.g., user navigated away)
// controller.abort();
// Practical: cancel on new search input
searchInput.addEventListener("input", (e) => {
controller.abort(); // cancel previous request
fetchResults(e.target.value);
});Streaming Responses
response.body is a ReadableStream — you can process data in chunks as it arrives instead of buffering the entire response in memory. This is essential for large files, streaming logs, or real-time data. Use TextDecoder for text streams. The reader.read() loop continues until done is true. Streaming avoids memory spikes on large payloads.
// Read a large response in chunks (streaming)
async function streamJson(url) {
const res = await fetch(url);
const reader = res.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 });
// Process complete lines
const lines = buffer.split("\n");
buffer = lines.pop(); // keep incomplete line
for (const line of lines) {
if (line.trim()) console.log(JSON.parse(line));
}
}
}
// Useful for: large files, NDJSON logs, real-time data feeds
// Processes data as it arrives instead of waiting for the full responseWeb Storage (LocalStorage & SessionStorage)
localStorage & sessionStorage Basics
localStorage persists indefinitely; sessionStorage clears when the tab closes. Both store strings only — use JSON.stringify/parse for objects. Storage is synchronous and blocks the main thread, so avoid storing large data. Available in all modern browsers but may be disabled in private browsing mode. Capacity is ~5-10MB per origin.
// Both store strings only — JSON.stringify objects
// localStorage: persists until cleared (survives browser restart)
// sessionStorage: cleared when the tab closes
// localStorage API
localStorage.setItem("username", "Alice");
const name = localStorage.getItem("username"); // "Alice"
localStorage.removeItem("username");
localStorage.clear(); // remove ALL items
// sessionStorage API (same interface, tab-scoped)
sessionStorage.setItem("tempToken", "abc123");
// Storing objects (must serialize)
const prefs = { theme: "dark", lang: "en" };
localStorage.setItem("prefs", JSON.stringify(prefs));
const loaded = JSON.parse(localStorage.getItem("prefs"));
console.log(loaded.theme); // "dark"
// Check if storage exists (private mode may disable it)
if (typeof Storage !== "undefined") {
// localStorage/sessionStorage available
}Storage Helper with TTL & JSON
Web Storage has no built-in expiration — this wrapper adds TTL (time-to-live) by storing an expiry timestamp alongside the value. This is the standard pattern for caching API responses or session data that should expire. Always wrap storage access in try/catch in production, as JSON.parse can throw on corrupted data and quota can be exceeded.
// A robust storage wrapper with expiration (TTL)
const store = {
set(key, value, ttlMs) {
const item = {
value: value,
expiry: ttlMs ? Date.now() + ttlMs : null,
};
localStorage.setItem(key, JSON.stringify(item));
},
get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const item = JSON.parse(raw);
if (item.expiry && Date.now() > item.expiry) {
localStorage.removeItem(key);
return null; // expired
}
return item.value;
},
remove(key) { localStorage.removeItem(key); },
};
// Usage: cache data for 5 minutes
store.set("userData", { name: "Alice" }, 5 * 60 * 1000);
console.log(store.get("userData")); // { name: "Alice" }
// After 5 min: returns null and cleans upStorage Events (Cross-Tab Sync)
The storage event is a built-in cross-tab communication channel — when one tab modifies localStorage, all other tabs on the same origin receive the event (but not the originating tab). This enables syncing state like login/logout, cart updates, or theme changes across tabs without WebSockets. The event includes key, oldValue, newValue, and url.
// The 'storage' event fires in OTHER tabs when storage changes
// (not in the tab that made the change)
window.addEventListener("storage", (event) => {
console.log("Key changed:", event.key);
console.log("Old value:", event.oldValue);
console.log("New value:", event.newValue);
console.log("URL:", event.url);
if (event.key === "cart") {
updateCartDisplay(JSON.parse(event.newValue));
}
});
// Practical: sync logout across tabs
// Tab A: localStorage.setItem("logout", Date.now());
// Tab B: receives storage event -> redirects to login
// Practical: broadcast a message to all tabs
function broadcast(type, data) {
localStorage.setItem("broadcast", JSON.stringify({ type, data, t: Date.now() }));
}
window.addEventListener("storage", (e) => {
if (e.key === "broadcast") {
const msg = JSON.parse(e.newValue);
handleMessage(msg);
}
});IndexedDB (Large Structured Storage)
IndexedDB is a powerful NoSQL database in the browser — async, transactional, and capable of storing much more data than localStorage (hundreds of MB). It supports indexes, cursors, and transactions. The raw API is callback-based and verbose; the 'idb' npm package provides a clean Promise-based wrapper. Use IndexedDB for offline-first apps, large caches, or complex client-side data.
// IndexedDB: async, transactional NoSQL store for large data
// Capacity: hundreds of MB to GB (far more than localStorage's 5MB)
// Open a database
const request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
// Create an object store (like a table) with a key path
if (!db.objectStoreNames.contains("users")) {
db.createObjectStore("users", { keyPath: "id" });
}
};
request.onsuccess = (e) => {
const db = e.target.result;
// Add data in a transaction
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
store.add({ id: 1, name: "Alice", age: 30 });
store.add({ id: 2, name: "Bob", age: 25 });
// Query
store.get(1).onsuccess = (e) => {
console.log(e.target.result); // { id: 1, name: "Alice", age: 30 }
};
};
// For easier use, consider the 'idb' library (Promise-based wrapper)Cookies vs Storage Comparison
Cookies are sent with every HTTP request (adding bandwidth) and are the standard for server-side auth (session IDs, CSRF tokens). localStorage/sessionStorage are client-only and hold much more data. IndexedDB is for large structured data. Choose based on: does the server need it? (cookie) How much data? (storage vs IndexedDB) How long? (session vs local). Set Secure, HttpOnly, and SameSite on security-sensitive cookies.
// COOKIES: sent with every HTTP request, ~4KB limit
document.cookie = "session=abc123; max-age=3600; path=/; Secure; SameSite=Strict";
console.log(document.cookie); // "session=abc123; theme=dark"
// Set cookie with attributes
document.cookie = "token=xyz; max-age=86400; Secure; HttpOnly; SameSite=Lax";
// Note: HttpOnly can't be set via JS (server-only for security)
// LOCAL STORAGE: ~5-10MB, NOT sent to server, synchronous
localStorage.setItem("theme", "dark");
// SESSION STORAGE: ~5MB, cleared on tab close
sessionStorage.setItem("draft", "work in progress");
// WHEN TO USE WHAT:
// Cookies -> auth tokens that the server needs to read, server-side sessions
// localStorage -> user preferences, cached data that persists
// sessionStorage -> temporary per-tab state (form drafts, wizard steps)
// IndexedDB -> large datasets, offline data, complex queriesTimers (setTimeout, setInterval)
setTimeout & setInterval
setTimeout runs a callback once after a delay; setInterval runs it repeatedly. Both return an ID for cancellation via clearTimeout/clearInterval. Timers are not precise — they're minimum delays subject to the event loop, tab visibility (throttled in background tabs), and main thread availability. Delays under 4ms may be clamped to 4ms in nested timers.
// setTimeout: run once after a delay (milliseconds)
const timeoutId = setTimeout(() => {
console.log("Runs after 2 seconds");
}, 2000);
// Cancel before it fires
clearTimeout(timeoutId);
// setInterval: run repeatedly every interval
const intervalId = setInterval(() => {
console.log("Runs every 1 second");
}, 1000);
// Stop the interval
clearInterval(intervalId);
// Pass arguments to the callback
setTimeout((greeting, name) => {
console.log(`${greeting}, ${name}`);
}, 1000, "Hello", "Alice");
// NOTE: timers don't guarantee exact timing — they run after the
// minimum delay, but only when the call stack is empty (event loop)Recursive setTimeout (Better than setInterval)
Recursive setTimeout is preferred over setInterval for recurring async tasks because it guarantees the previous call finishes before the next starts — no overlapping executions. It also allows dynamic intervals (e.g., longer backoff on errors). setInterval can stack calls if the handler takes longer than the interval, causing performance issues.
// setInterval problems: doesn't wait for the previous call to finish
// If a call takes longer than the interval, they can stack up.
// Better: recursive setTimeout — guarantees the gap between completions
function poll() {
fetch("/api/status")
.then(r => r.json())
.then(data => {
console.log(data);
setTimeout(poll, 1000); // schedule next AFTER this finishes
})
.catch(err => {
console.error(err);
setTimeout(poll, 5000); // retry with backoff on error
});
}
poll(); // start the loop
// This pattern: always waits for the previous call to complete
// before scheduling the next, avoiding overlap/stacking.requestAnimationFrame (Smooth Animations)
requestAnimationFrame (rAF) is the correct way to do visual animations — it syncs with the browser's repaint cycle (~60fps), avoids unnecessary frames when the tab is hidden, and produces smoother animations than setInterval. Use the timestamp parameter for delta-time calculations to keep animation speed consistent across different refresh rates. Always cancel with cancelAnimationFrame when done.
// requestAnimationFrame: syncs with the display refresh (~60fps)
// More efficient than setInterval for visual animations
function animate() {
element.style.left = parseInt(element.style.left || 0) + 2 + "px";
if (parseInt(element.style.left) < 300) {
requestAnimationFrame(animate); // schedule next frame
}
}
requestAnimationFrame(animate);
// Cancel if needed
const rafId = requestAnimationFrame(animate);
cancelAnimationFrame(rafId);
// Timestamp for delta-time calculations
let lastTime = 0;
function loop(timestamp) {
const delta = timestamp - lastTime;
lastTime = timestamp;
update(delta); // frame-rate independent movement
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// rAF pauses in background tabs (saves CPU/battery)Debounce & Throttle
Debounce waits for a pause in calls before executing (e.g., only search 300ms after the user stops typing). Throttle limits execution to once per interval (e.g., update scroll position at most every 200ms). Both prevent performance issues from high-frequency events. Debounce = 'group rapid calls into one'; throttle = 'cap the rate of calls'.
// Debounce: delay execution until calls stop for N ms
// Use for: search input, window resize, button spam
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer); // reset the countdown
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((query) => {
fetchResults(query);
}, 300);
input.addEventListener("input", (e) => search(e.target.value));
// Throttle: execute at most once per N ms
// Use for: scroll, mousemove, drag handlers
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
const onScroll = throttle(() => {
console.log("Scroll position:", window.scrollY);
}, 200);
window.addEventListener("scroll", onScroll);Promisified setTimeout (delay)
Wrapping setTimeout in a Promise creates a clean 'delay' function for async/await — far more readable than callback chains. This pattern powers retry logic with exponential backoff (wait 1s, 2s, 4s between retries), sequential animations, and rate limiting. The delay helper is one of the most useful tiny utilities in modern async JS.
// A promise-based delay for use with async/await
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function countdown() {
console.log("3...");
await delay(1000);
console.log("2...");
await delay(1000);
console.log("1...");
await delay(1000);
console.log("Go!");
}
countdown();
// Retry with exponential backoff
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (res.ok) return await res.json();
throw new Error(`HTTP ${res.status}`);
} catch (err) {
if (i === retries - 1) throw err;
const wait = Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.log(`Retry ${i + 1} in ${wait}ms`);
await delay(wait);
}
}
}Map, Set & WeakMap
Map (Key-Value with Any Key Type)
Map is a proper key-value collection where keys can be any type (objects, functions, numbers) — unlike Objects which coerce keys to strings. Map preserves insertion order, has a .size property, and is directly iterable. Use Map when you need non-string keys, frequent additions/deletions, or when the collection isn't a record/DTO. Use Object for fixed-shape data.
// Map: like an Object but keys can be ANY type (not just strings)
const map = new Map();
// Keys can be objects, functions, primitives
const objKey = { id: 1 };
map.set("name", "Alice");
map.set(objKey, "data for this object");
map.set(42, "numeric key");
map.set(true, "boolean key");
console.log(map.get("name")); // "Alice"
console.log(map.get(objKey)); // "data for this object"
console.log(map.size); // 4
console.log(map.has(42)); // true
// Iteration preserves insertion order
for (const [key, value] of map) {
console.log(key, value);
}
// Map vs Object:
// - Map keys can be any type; Object keys are strings/symbols
// - Map has .size; Object needs Object.keys().length
// - Map is iterable by default; Object needs Object.entries()
// - Map performs better for frequent add/removeSet (Unique Values)
Set stores unique values — perfect for deduplication and membership testing. Converting an array to Set and back ([...new Set(arr)]) is the idiomatic way to remove duplicates. Set.has() is O(1) vs Array.includes() which is O(n), so use Set for large collections you check frequently. Set doesn't have map/filter — spread to array first.
// Set: collection of unique values (no duplicates)
const set = new Set([1, 2, 3, 2, 1]);
console.log(set); // Set(3) { 1, 2, 3 }
console.log(set.size); // 3
set.add(4);
set.add(4); // ignored (already exists)
console.log(set.has(3)); // true
set.delete(2);
// Iterate (preserves insertion order)
for (const item of set) {
console.log(item);
}
// Common use cases:
// 1. Remove duplicates from an array
const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]
// 2. Check for membership (faster than Array.includes for large sets)
const validIds = new Set([101, 102, 103]);
if (validIds.has(userId)) { /* ... */ }
// 3. Set operations (union, intersection)
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]); // {1,2,3,4}
const intersect = new Set([...a].filter(x => b.has(x))); // {2,3}WeakMap & WeakSet (Memory-Safe References)
WeakMap and WeakSet hold weak references to objects — when no other references exist, the entry is garbage collected automatically. This prevents memory leaks when associating data with DOM elements or other objects. Keys must be objects. WeakMap/WeakSet are not iterable and have no .size because entries can disappear at any time during GC. Use them for caching/metadata tied to object lifetimes.
// WeakMap: keys must be objects; references are "weak"
// (doesn't prevent garbage collection of the key object)
const weakMap = new WeakMap();
let user = { name: "Alice" };
weakMap.set(user, "metadata");
console.log(weakMap.get(user)); // "metadata"
user = null; // remove the only strong reference
// Now the object can be GC'd, and WeakMap entry disappears automatically
// Practical: attach data to DOM elements without memory leaks
const elementData = new WeakMap();
function cacheData(element, data) {
elementData.set(element, data);
}
// When the DOM element is removed, its data is auto-cleaned
// WeakSet: collection of objects, weakly held
const processed = new WeakSet();
function processOnce(obj) {
if (processed.has(obj)) return; // already done
processed.add(obj);
doWork(obj);
}
// WeakMap/WeakSet are NOT iterable (no .size, no for...of)Map Iteration & Conversion
Maps are iterable in insertion order via for...of (entries by default), .keys(), .values(), or .forEach(). Convert between Maps and Objects using Object.entries() and Object.fromEntries(). This bidirectional conversion is handy when APIs expect plain objects but you want Map's features internally. Remember: Object keys become string type in the conversion.
const map = new Map([
["name", "Alice"],
["age", 30],
["city", "NYC"],
]);
// Iterate entries (default)
for (const [key, value] of map) {
console.log(`${key} = ${value}`);
}
// Iterate keys only
for (const key of map.keys()) {
console.log(key);
}
// Iterate values only
for (const value of map.values()) {
console.log(value);
}
// forEach (callback style)
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// Convert to Array
const entries = [...map.entries()]; // [["name","Alice"], ["age",30], ...]
const keys = [...map.keys()]; // ["name", "age", "city"]
const values = [...map.values()]; // ["Alice", 30, "NYC"]
// Convert Object to Map and back
const obj = { a: 1, b: 2 };
const mapFromObj = new Map(Object.entries(obj));
const objFromMap = Object.fromEntries(map);Choosing Between Map, Set, Object & Array
Choose the right collection: Arrays for ordered lists with duplicates and index access; Objects for fixed-shape records and JSON; Maps for dynamic key-value collections with any key type; Sets for uniqueness and fast lookup. Using the wrong structure leads to verbose code and performance issues — e.g., checking Array.includes() in a loop vs Set.has().
// Decision guide:
// Use ARRAY when:
// - You need ordered data with duplicates
// - You need index-based access (arr[5])
// - You'll map/filter/reduce frequently
const todoList = ["buy milk", "walk dog"];
// Use OBJECT when:
// - You have a fixed known shape (like a user record)
// - You need JSON serialization (JSON.stringify)
// - Keys are strings and known at write time
const user = { name: "Alice", age: 30 };
// Use MAP when:
// - Keys are not strings (objects, functions, numbers)
// - You frequently add/remove key-value pairs
// - You need to iterate in insertion order
// - The collection size changes dynamically
const handlers = new Map();
handlers.set(buttonElement, () => onClick());
// Use SET when:
// - You need unique values (no duplicates)
// - You need fast membership testing (.has is O(1))
// - Order doesn't matter much
const seen = new Set();
if (!seen.has(url)) { seen.add(url); visit(url); }Generators & Iterators
Iterators & Symbol.iterator
An iterator has a next() method returning {value, done}. An iterable implements Symbol.iterator, which returns an iterator. Built-in iterables (arrays, strings, Maps, Sets) work with for...of, spread (...), destructuring, and Array.from(). Implementing Symbol.iterator lets your custom objects work with all these language features seamlessly.
// An iterator is an object with a next() method returning {value, done}
function makeIterator(arr) {
let i = 0;
return {
next() {
return i < arr.length
? { value: arr[i++], done: false }
: { value: undefined, done: true };
},
};
}
const it = makeIterator(["a", "b", "c"]);
console.log(it.next()); // { value: "a", done: false }
console.log(it.next()); // { value: "b", done: false }
console.log(it.next()); // { value: "c", done: false }
console.log(it.next()); // { value: undefined, done: true }
// An iterable implements Symbol.iterator (returns an iterator)
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { done: true };
},
};
},
};
// Now range works with for...of, spread, etc.
console.log([...range]); // [1, 2, 3]Generator Functions (function*)
Generator functions (function*) use yield to pause and resume execution — they produce values lazily, one at a time. This enables infinite sequences, lazy evaluation, and memory-efficient pipelines. Generators are both iterators and iterables. Each call to next() runs until the next yield (or return). They're the foundation of async generators and JS coroutine patterns.
// Generators: pause execution with 'yield', resume with next()
function* idGenerator() {
let id = 1;
while (true) {
yield id++; // pause here, return value, resume on next()
}
}
const gen = idGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
// Infinite sequence — only computes values on demand
// Generators are iterables (work with for...of)
function* take(n, iterable) {
for (const item of iterable) {
if (n-- <= 0) return;
yield item;
}
}
function* naturalNumbers() {
let n = 1;
while (true) yield n++;
}
const firstFive = [...take(5, naturalNumbers())];
console.log(firstFive); // [1, 2, 3, 4, 5]yield* (Delegate to Another Generator)
yield* delegates all yields to another iterable or generator — flattening nested structures and composing generators cleanly. It's the JS equivalent of Python's 'yield from'. The delegated generator's values are yielded one by one as if they were part of the outer generator. This is the standard way to compose generators recursively.
// yield* delegates to another iterable/generator
function* inner() {
yield "a";
yield "b";
}
function* outer() {
yield 1;
yield* inner(); // delegates: yields "a", then "b"
yield* [10, 20]; // also works with arrays
yield 2;
}
console.log([...outer()]); // [1, "a", "b", 10, 20, 2]
// Practical: flatten nested structures
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // recursive delegation
} else {
yield item;
}
}
}
const nested = [1, [2, [3, 4]], 5];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5]Generator send() via next(value)
Generators support two-way communication: next(value) passes a value in (becomes the result of the last yield expression), and throw(error) injects an exception at the yield point. This enables coroutines, state machines, and the generator-based async patterns that powered early async/await implementations. The first next() call can't pass a value in (nothing to receive it yet).
// next(value) passes a value INTO the generator (becomes yield's result)
function* conversation() {
const name = yield "What's your name?"; // receives via next()
const age = yield `Hello ${name}, how old are you?`;
return `${name} is ${age} years old`;
}
const talk = conversation();
console.log(talk.next().value); // "What's your name?"
console.log(talk.next("Alice").value); // "Hello Alice, how old are you?"
console.log(talk.next(30).value); // "Alice is 30 years old"
// throw(error) injects an exception at the yield point
function* safeGen() {
try {
yield "step 1";
} catch (e) {
console.log("Caught:", e.message);
yield "recovered";
}
}
const g = safeGen();
g.next(); // "step 1"
g.throw(new Error("oops")); // "Caught: oops" -> "recovered"Async Generators
Async generators (async function*) combine generators with async/await — each yield can produce a value after awaiting. Consume them with for await...of. This is ideal for paginated APIs, streaming data, or any scenario where you produce values asynchronously. They're the modern way to handle streaming data in JS without callbacks or manual promise chaining.
// async function* : yields promises, can use await
async function* fetchPages(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.items.length === 0) break; // no more pages
yield data.items;
page++;
}
}
// Consume with for await...of
(async () => {
for await (const items of fetchPages("/api/products")) {
console.log(`Got ${items.length} items`);
renderItems(items);
}
console.log("All pages loaded");
})();
// Practical: stream data as it arrives
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();
for (const line of lines) yield line;
}
}Proxy & Reflect
Proxy Basics (Intercept Operations)
Proxy lets you intercept and customize fundamental operations on an object — get, set, has, deleteProperty, ownKeys, and more. The handler object defines 'traps' (like getters/setters but for all properties). Use cases: validation, logging, default values, access control, reactive systems (Vue 3 uses Proxy for reactivity). Return true from set to indicate success in strict mode.
// Proxy wraps an object and intercepts fundamental operations
const target = { name: "Alice", age: 30 };
const handler = {
get(obj, prop) {
console.log(`Reading ${prop}`);
return prop in obj ? obj[prop] : "N/A"; // default value
},
set(obj, prop, value) {
console.log(`Setting ${prop} = ${value}`);
if (prop === "age" && value < 0) return false; // validation
obj[prop] = value;
return true; // indicate success
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // logs "Reading name", returns "Alice"
proxy.age = 25; // logs "Setting age = 25"
// proxy.age = -5; // logs "Setting age = -5", rejected
console.log(proxy.unknown); // logs "Reading unknown", returns "N/A"Common Proxy Use Cases
Proxies enable powerful metaprogramming: negative indices, lazy property computation, read-only views, validation, logging, and reactive data binding. Vue 3's reactivity system uses Proxy to track property access and trigger re-renders automatically. The trade-off is a small performance overhead, so use Proxies where the abstraction is worth it, not for every object.
// 1. Negative array indices (like Python)
function negativeArray(arr) {
return new Proxy(arr, {
get(target, prop) {
const idx = Number(prop);
if (Number.isInteger(idx)) {
return target[idx < 0 ? target.length + idx : idx];
}
return target[prop];
},
});
}
const arr = negativeArray(["a", "b", "c"]);
console.log(arr[-1]); // "c"
// 2. Auto-populating / lazy properties
const lazy = new Proxy({}, {
get(target, prop) {
if (!(prop in target)) {
target[prop] = expensiveCompute(prop); // compute on first access
}
return target[prop];
},
});
// 3. Read-only object
const readOnly = new Proxy(data, {
set() { throw new Error("This object is read-only"); },
deleteProperty() { throw new Error("Cannot delete"); },
});Reflect API
Reflect provides the same operations that Proxy traps intercept — use it inside traps to forward to default behavior cleanly. Reflect methods return booleans (success/failure) instead of throwing, making them safer for conditional logic. Reflect.ownKeys returns all keys (strings AND symbols), unlike Object.keys which only returns enumerable string keys. Together, Proxy and Reflect form JS's metaprogramming toolkit.
// Reflect provides default behavior for proxy traps and
// functional equivalents of object operations
const obj = { x: 1, y: 2 };
// Reflect.get / Reflect.set (instead of obj[prop])
console.log(Reflect.get(obj, "x")); // 1
Reflect.set(obj, "z", 3); // obj.z = 3
console.log(Reflect.has(obj, "x")); // true (like "x" in obj)
Reflect.deleteProperty(obj, "y"); // delete obj.y
console.log(Reflect.ownKeys(obj)); // ["x", "z"]
// In a Proxy, use Reflect to forward to the default behavior
const proxy = new Proxy({}, {
get(target, prop, receiver) {
console.log(`get ${prop}`);
return Reflect.get(target, prop, receiver); // default behavior
},
set(target, prop, value, receiver) {
console.log(`set ${prop}`);
return Reflect.set(target, prop, value, receiver);
},
});
// Reflect.construct: call a constructor with an array of args
const instance = Reflect.construct(Array, [1, 2, 3]);Reactive Object with Proxy (Vue-style)
This is the core idea behind Vue 3's reactivity — a Proxy intercepts get (to track which effects depend on a property) and set (to trigger those effects when the property changes). This enables declarative UI updates: you mutate state, and the framework re-renders automatically. Proxies made this possible in ES6; earlier frameworks (Vue 2) used Object.defineProperty with limitations.
// A minimal reactive system using Proxy
function reactive(target) {
const subscribers = new Set();
const handler = {
get(obj, prop) {
track(prop); // record who reads this property
const value = Reflect.get(obj, prop);
return typeof value === "object" && value !== null
? reactive(value) // deeply reactive
: value;
},
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
trigger(prop); // notify subscribers
return result;
},
};
let currentEffect = null;
function track(prop) {
if (currentEffect) subscribers.add(currentEffect);
}
function trigger(prop) {
subscribers.forEach(fn => fn());
}
return new Proxy(target, handler);
}
const state = reactive({ count: 0 });
// When state.count changes, effects re-run automaticallyWeb Workers
Creating a Web Worker
Web Workers run JavaScript in a separate background thread, enabling true parallelism without blocking the UI. The main thread and worker communicate via postMessage (data is copied/structured-cloned, not shared). Workers can't access the DOM or window object — they're isolated. Use workers for CPU-intensive tasks like image processing, parsing large files, or complex calculations.
// main.js — workers run JS in a background thread
const worker = new Worker("worker.js");
// Send data to the worker
worker.postMessage({ command: "calculate", data: [1, 2, 3, 4, 5] });
// Receive results from the worker
worker.onmessage = (event) => {
console.log("Result from worker:", event.data);
};
worker.onerror = (error) => {
console.error("Worker error:", error.message);
};
// Terminate when done (frees resources)
// worker.terminate();
// --- worker.js (separate file) ---
// self.onmessage = (event) => {
// const { command, data } = event.data;
// const result = heavyComputation(data);
// self.postMessage(result);
// };Inline Workers (Blob URL)
Inline workers create a Worker from a code string via a Blob URL — no separate file needed. This is handy for demos, small utilities, or when your build system doesn't easily handle separate worker files. Remember to revoke the object URL to avoid memory leaks. The worker code is a string, so you lose editor syntax highlighting and type checking — use with care in production.
// Create a worker from a string (no separate file needed)
const workerCode = `
self.onmessage = function(e) {
const result = e.data.map(x => x * x);
self.postMessage(result);
};
`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
worker.postMessage([1, 2, 3, 4]);
worker.onmessage = (e) => console.log(e.data); // [1, 4, 9, 16]
// Clean up the blob URL when done
// URL.revokeObjectURL(blob URL);
// Useful for: single-file demos, bundlers that inline workers,
// or when you can't serve a separate .js fileTransferable Objects (Zero-Copy)
Normally, postMessage copies data (structured clone), which is slow for large buffers. The transfer list (second argument) MOVES ownership of ArrayBuffer/MessagePort/ImageBitmap to the worker with zero copying — near-instant regardless of size. The original buffer becomes detached (unusable). Use this for large datasets, image processing, or audio to avoid the copy overhead.
// Transferable objects move data to a worker WITHOUT copying
// (the original becomes unusable) — much faster for large ArrayBuffers
// Create a large buffer
const buffer = new ArrayBuffer(1024 * 1024 * 10); // 10MB
const view = new Float64Array(buffer);
view[0] = 3.14;
// Transfer the buffer (second arg = transfer list)
worker.postMessage({ data: buffer }, [buffer]);
// After transfer, 'buffer' is detached (length becomes 0)
console.log(buffer.byteLength); // 0 — ownership moved to worker
// Worker receives it normally:
// self.onmessage = (e) => {
// const buf = e.data.data; // full 10MB, no copy
// };
// Transferable types: ArrayBuffer, MessagePort, ImageBitmap
// Use for: large datasets, image data, audio buffersSharedArrayBuffer & Atomics
SharedArrayBuffer allows true shared memory between threads (no copying), and Atomics provides thread-safe operations (add, load, store, compareExchange, wait/notify) on it. This enables high-performance parallel algorithms in JS. Due to Spectre security concerns, SharedArrayBuffer requires cross-origin isolation HTTP headers — without them, it's disabled in modern browsers. Use for WASM interop and heavy parallel computation.
// SharedArrayBuffer: memory shared between main thread and workers
// (both can read/write simultaneously) — true shared memory
// const sharedBuffer = new SharedArrayBuffer(1024);
// const view = new Int32Array(sharedBuffer);
// worker.postMessage({ buffer: sharedBuffer });
// Atomics: thread-safe operations on SharedArrayBuffer
// const sharedArray = new Int32Array(sharedBuffer);
// Atomics.add(sharedArray, 0, 1); // atomic increment
// Atomics.load(sharedArray, 0); // atomic read
// Atomics.store(sharedArray, 0, 42); // atomic write
// Atomics.compareExchange(sharedArray, 0, 42, 99); // CAS
// Atomics.wait / notify: block and wake threads
// Worker:
// Atomics.wait(sharedArray, 0, 0); // block until index 0 != 0
// Main:
// Atomics.store(sharedArray, 0, 1);
// Atomics.notify(sharedArray, 0); // wake waiting workers
// NOTE: Requires cross-origin isolation headers:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corpWorker Pool Pattern
Creating workers has overhead, so a worker pool reuses a fixed number of workers and queues tasks — like a thread pool in other languages. This pattern maximizes CPU utilization (one worker per core) while avoiding the cost of spawning workers per task. The pool dispatches tasks to free workers and queues them when all are busy. Essential for processing many independent chunks of work.
// A worker pool reuses workers to avoid creation overhead
class WorkerPool {
constructor(workerUrl, size = 4) {
this.workers = [];
this.queue = [];
for (let i = 0; i < size; i++) {
const worker = new Worker(workerUrl);
worker.busy = false;
this.workers.push(worker);
}
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const worker = this.workers.find(w => !w.busy);
if (worker) {
this.execute(worker, task);
} else {
this.queue.push(task); // wait for a free worker
}
});
}
execute(worker, task) {
worker.busy = true;
worker.onmessage = (e) => {
worker.busy = false;
task.resolve(e.data);
this.next();
};
worker.postMessage(task.data);
}
next() {
const task = this.queue.shift();
const worker = this.workers.find(w => !w.busy);
if (task && worker) this.execute(worker, task);
}
}DOM Manipulation Deep
querySelector
querySelector returns the first match, querySelectorAll returns a static NodeList. getElementsByClassName returns a live HTMLCollection. NodeList supports forEach; HTMLCollection does not.
const el = document.querySelector('.my-class');
const all = document.querySelectorAll('div.item');
all.forEach(el => console.log(el.textContent));
const live = document.getElementsByClassName('item'); // live
const static = document.querySelectorAll('.item'); // staticCreate & Insert
createElement creates a new element. appendChild adds as last child, prepend adds as first. textContent is safer than innerHTML as it prevents XSS.
const div = document.createElement('div');
div.className = 'card';
div.textContent = 'Hello';
div.setAttribute('data-id', '42');
const parent = document.querySelector('#container');
parent.appendChild(div);
parent.prepend(div);Event Delegation
Event delegation attaches one listener to a parent instead of many to children. closest finds the nearest ancestor matching a selector. Handles dynamically added elements.
document.querySelector('#list').addEventListener('click', (e) => {
const item = e.target.closest('.list-item');
if (!item) return;
console.log('Clicked:', item.dataset.id);
});classList API
classList provides methods to manipulate CSS classes safely. toggle returns true if added, false if removed. Cleaner than manipulating className string.
const el = document.querySelector('.box');
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('dark-mode');
el.classList.replace('old', 'new');
if (el.classList.contains('active')) { /* ... */ }Dataset Attributes
data-* attributes store custom data. dataset provides camelCase access: data-user-id becomes dataset.userId. Values are always strings.
// HTML: <div data-user-id="42" data-role="admin"></div>
const el = document.querySelector('[data-user-id]');
console.log(el.dataset.userId); // "42"
console.log(el.dataset.role); // "admin"
el.dataset.userId = '99';Canvas API
Basic Drawing
getContext("2d") returns the 2D rendering context. fillRect draws a filled rectangle, strokeRect draws an outline. Set fillStyle/strokeStyle before drawing.
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 50);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.strokeRect(130, 10, 100, 50);Paths & Lines
moveTo positions the cursor without drawing. lineTo draws a line. closePath connects back to start. arc draws circles/arcs.
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.lineTo(100, 150);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.arc(250, 35, 25, 0, Math.PI * 2);
ctx.fill();Text & Gradients
createLinearGradient creates a gradient. addColorStop defines colors at positions (0-1). Set font and textAlign before drawing text.
const grad = ctx.createLinearGradient(0, 0, 200, 0);
grad.addColorStop(0, 'red');
grad.addColorStop(1, 'blue');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 200, 100);
ctx.font = '24px Arial';
ctx.fillText('Hello Canvas', 100, 50);Animation Loop
requestAnimationFrame syncs with display refresh (~60fps). clearRect clears before each frame. Cancel with cancelAnimationFrame.
let x = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'green';
ctx.fillRect(x, 50, 30, 30);
x += 2;
if (x > canvas.width) x = 0;
requestAnimationFrame(animate);
}
animate();Image Manipulation
drawImage renders images. getImageData returns pixel data as RGBA arrays. Manipulate pixels for filters. putImageData writes modified pixels back.
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, 200, 150);
const data = ctx.getImageData(0, 0, 200, 150);
for (let i = 0; i < data.data.length; i += 4)
data.data[i] = 255 - data.data[i];
ctx.putImageData(data, 0, 0);
};
img.src = 'photo.jpg';WebSockets
Basic WebSocket
WebSocket provides full-duplex communication over a single TCP connection. onopen fires when connected, onmessage when data arrives. Always handle onerror.
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => { ws.send('Hello Server'); };
ws.onmessage = (e) => { console.log('Received:', e.data); };
ws.onclose = () => console.log('Disconnected');
ws.onerror = (err) => console.error('Error:', err);Send & Receive JSON
WebSocket data is transmitted as strings or binary. JSON.stringify/parse enables structured data exchange. A type field enables message routing.
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'message', text: 'Hello' }));
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
switch (data.type) {
case 'message': console.log(data.text); break;
}
};Reconnection
WebSocket connections can drop. Exponential backoff (2^retries) prevents overwhelming the server. Cap delay at 30s. Reset retry count on success.
class ReconnectingWS {
constructor(url) { this.url = url; this.retries = 0; this.connect(); }
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.retries = 0; };
this.ws.onclose = () => {
const delay = Math.min(1000 * 2 ** this.retries++, 30000);
setTimeout(() => this.connect(), delay);
};
}
}Binary Data
WebSocket supports binary data via ArrayBuffer. Set binaryType to arraybuffer. DataView provides typed access. Binary is more efficient for numeric data.
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setInt32(0, 42);
ws.send(buffer);
ws.binaryType = 'arraybuffer';
ws.onmessage = (e) => {
if (e.data instanceof ArrayBuffer) {
const v = new DataView(e.data);
console.log(v.getInt32(0));
}
};Heartbeat
Heartbeats detect stale connections. Send pings and expect pongs. If no pong within timeout, close and reconnect. readyState checks connection status.
setInterval(() => {
if (ws.readyState === WebSocket.OPEN)
ws.send(JSON.stringify({ type: 'ping' }));
}, 30000);
setInterval(() => {
if (Date.now() - lastPong > 60000) ws.close();
}, 10000);Service Workers
Registration
Service workers run in a separate thread, intercepting network requests. Registration must happen on HTTPS or localhost. The SW file location determines scope.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('Registered:', reg.scope))
.catch(err => console.error('Failed:', err));
}Caching
Cache-first strategy: serve from cache, fall back to network. install pre-caches assets. fetch intercepts requests. Other strategies: network-first, stale-while-revalidate.
const CACHE = 'app-v1';
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE)
.then(c => c.addAll(['/', '/index.html', '/style.css'])));
});
self.addEventListener('fetch', (e) => {
e.respondWith(caches.match(e.request)
.then(cached => cached || fetch(e.request)));
});Background Sync
Background sync defers actions until connectivity returns. The sync event fires when network is available. Store pending actions in IndexedDB.
navigator.serviceWorker.ready.then(reg =>
reg.sync.register('send-messages'));
self.addEventListener('sync', (e) => {
if (e.tag === 'send-messages')
e.waitUntil(sendPendingMessages());
});Push Notifications
Push notifications work even when the app is closed. subscribe registers with a push service using VAPID keys. userVisibleOnly requires showing a notification.
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey
});
self.addEventListener('push', (e) => {
const data = e.data.json();
e.waitUntil(self.registration.showNotification(data.title, {
body: data.body
}));
});Update & Activate
skipWaiting activates a new SW immediately. activate cleans up old caches. clients.claim takes control immediately. Version the cache name to trigger updates.
self.addEventListener('install', (e) => {
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE)
.map(k => caches.delete(k)))));
self.clients.claim();
});IndexedDB
Open Database
IndexedDB is a NoSQL database in the browser. onupgradeneeded fires when version changes, used to create object stores. keyPath defines the primary key.
const req = indexedDB.open('MyDatabase', 1);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('users'))
db.createObjectStore('users', { keyPath: 'id' });
};
req.onsuccess = (e) => { const db = e.target.result; };Add & Put
Transactions group operations atomically. add fails on duplicate keys, put overwrites. readwrite allows modifications. oncomplete fires on success.
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.add({ id: 1, name: 'Alice' }); // Fails if key exists
store.put({ id: 1, name: 'Bob' }); // Overwrites
tx.oncomplete = () => console.log('Saved');Query Data
get retrieves by key. openCursor iterates records. cursor.continue moves to next. Wrap in Promises for async/await usage.
const tx = db.transaction('users', 'readonly');
const req = tx.objectStore('users').get(1);
req.onsuccess = () => console.log(req.result);
const cursorReq = tx.objectStore('users').openCursor();
cursorReq.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) { console.log(cursor.value); cursor.continue(); }
};Index Queries
Indexes enable queries on non-key fields. IDBKeyRange creates bounds. Indexes must be created in onupgradeneeded.
const tx = db.transaction('users', 'readonly');
const index = tx.objectStore('users').index('name');
index.get('Alice').onsuccess = (e) =>
console.log(e.target.result);
const range = IDBKeyRange.bound('A', 'M');
index.openCursor(range).onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) { cursor.continue(); }
};Delete & Clear
delete removes a single record. clear removes all records. deleteDatabase removes the entire database. All modifications require readwrite transactions.
const tx = db.transaction('users', 'readwrite');
tx.objectStore('users').delete(1); // Delete by key
tx.objectStore('users').clear(); // Clear all
indexedDB.deleteDatabase('MyDatabase'); // Delete DB
tx.oncomplete = () => console.log('Done');WebRTC
Get User Media
getUserMedia requests camera and microphone access. Returns a MediaStream. srcObject assigns stream to video element. Requires HTTPS and user permission.
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
document.querySelector('video').srcObject = stream;
});Peer Connection
RTCPeerConnection establishes P2P connections. addTrack adds media. ontrack receives remote stream. ICE candidates are network paths exchanged via signaling.
const pc = new RTCPeerConnection(config);
stream.getTracks().forEach(t => pc.addTrack(t, stream));
pc.ontrack = (e) => { remoteVideo.srcObject = e.streams[0]; };
pc.onicecandidate = (e) => {
if (e.candidate) sendToPeer(e.candidate);
};Offer & Answer
SDP negotiation: offer/answer exchange describes media formats. setLocalDescription sets local SDP, setRemoteDescription sets remote SDP.
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendToPeer(offer);
// Callee:
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToPeer(answer);Data Channels
Data channels enable arbitrary data transfer over WebRTC with low latency. createDataChannel creates on offerer. ondatachannel receives on answerer.
const channel = pc.createDataChannel('chat');
channel.onopen = () => console.log('Connected');
channel.onmessage = (e) => console.log('Received:', e.data);
channel.send('Hello peer!');
pc.ondatachannel = (e) => {
e.channel.onmessage = (ev) => console.log(ev.data);
};Screen Sharing
getDisplayMedia captures screen, window, or tab. The browser shows a picker. onended fires when user stops sharing. Always handle cleanup.
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 30 }, audio: true
});
video.srcObject = stream;
stream.getVideoTracks()[0].onended = () =>
console.log('Stopped');Performance Optimization
Debounce & Throttle
Debounce delays execution until calls stop (good for search). Throttle limits to once per interval (good for scroll). Both prevent excessive calls.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) { fn(...args); inThrottle = true;
setTimeout(() => inThrottle = false, limit); }
};
}Web Workers
Web Workers run JavaScript in a separate thread for CPU-intensive tasks. Data is passed via postMessage. Workers cannot access the DOM.
const worker = new Worker('worker.js');
worker.postMessage({ data: [1, 2, 3] });
worker.onmessage = (e) => console.log('Result:', e.data);
// worker.js
self.onmessage = (e) => {
self.postMessage(e.data.data.map(x => x ** 2));
};Lazy Loading
IntersectionObserver fires when elements enter viewport. data-src holds real URL; src set when visible. Reduces initial page load for image-heavy pages.
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));Batch DOM Updates
DocumentFragment batches DOM insertions into a single reflow, much faster than appending one by one. Minimize layout thrashing.
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
list.appendChild(fragment); // Single reflowMemory Management
WeakMap allows GC of keys, preventing leaks. Always remove event listeners when elements are removed. Set large objects to null for GC.
const cache = new WeakMap();
cache.set(element, data);
// When element is GC'd, entry is removed
element.removeEventListener('click', handler);
bigArray = null; // Allow GCSecurity (XSS/CSRF)
XSS Prevention
XSS injects malicious scripts via user input. Never use innerHTML with untrusted data. textContent is safe. CSP headers restrict script sources.
// BAD: vulnerable to XSS
element.innerHTML = userInput;
// GOOD: safe alternatives
element.textContent = userInput;
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}CSRF Prevention
CSRF tricks users into unwanted actions. Tokens ensure requests came from your app. SameSite=Strict cookies are not sent cross-site. Use CSRF tokens for state-changing operations.
const token = document.cookie.match(/csrf_token=([^;]+)/)?.[1];
fetch('/api/data', {
method: 'POST',
headers: { 'X-CSRF-Token': token }
});
// SameSite cookie: Set-Cookie: session=abc; SameSite=StrictContent Security Policy
CSP restricts which resources can load. default-src is the fallback. script-src controls JavaScript. Start with report-only mode before enforcing.
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data:">Secure Cookies
HttpOnly prevents JavaScript access to cookies, mitigating XSS. Secure ensures HTTPS only. SameSite=Strict prevents CSRF. Session cookies should always use both.
// Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
// HttpOnly: not accessible via JavaScript
// Secure: only sent over HTTPS
// SameSite: Strict | Lax | None
document.cookie // Cannot see HttpOnly cookiesInput Validation
Client-side validation improves UX but is not security. Always validate on server. Use DOMPurify for HTML sanitization. Whitelist allowed tags.
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function sanitize(html) {
return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong'] });
}
// Never trust client input - always validate on server tooDesign Patterns
Singleton
Singleton ensures only one instance exists. The constructor returns the existing instance. Useful for configuration, logging, and database connections.
class Config {
constructor() {
if (Config.instance) return Config.instance;
this.settings = {};
Config.instance = this;
}
}
const c1 = new Config();
const c2 = new Config();
console.log(c1 === c2); // trueObserver/Pub-Sub
Observer pattern lets objects subscribe to events. on registers, emit triggers, off unsubscribes. Foundation of event-driven architectures.
class EventEmitter {
constructor() { this.events = {}; }
on(event, cb) { (this.events[event] ||= []).push(cb); }
emit(event, data) { (this.events[event] || []).forEach(cb => cb(data)); }
off(event, cb) { this.events[event] = this.events[event]?.filter(c => c !== cb); }
}Factory
Factory creates objects without exposing instantiation logic. The caller specifies a type, the factory decides which class to instantiate.
class Dog { speak() { return 'Woof'; } }
class Cat { speak() { return 'Meow'; } }
function AnimalFactory(type) {
switch (type) {
case 'dog': return new Dog();
case 'cat': return new Cat();
}
}Module Pattern
The module pattern encapsulates private state using closures. IIFE creates a private scope. Only returned methods are public.
const counter = (() => {
let count = 0; // Private
return {
increment: () => ++count,
getCount: () => count
};
})();
counter.increment();
console.log(counter.getCount()); // 1Strategy
Strategy pattern encapsulates interchangeable algorithms. The context delegates to the selected strategy. Avoids large if/else chains.
const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
};
function calculate(strategy, a, b) {
return strategies[strategy]?.(a, b);
}Common Pitfalls
this Binding
this is determined by how a function is called. Arrow functions inherit this from enclosing scope. Methods lose this when detached. Use bind.
const obj = {
name: 'Alice',
greet: function() { console.log(this.name); },
arrow: () => console.log(this.name)
};
obj.greet(); // Alice
obj.arrow(); // undefined
const fn = obj.greet;
fn(); // undefined (lost binding)Floating Point
JavaScript uses IEEE 754 floating point. Use Number.EPSILON for comparisons, or multiply by powers of 10 to work with integers.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
const eps = Number.EPSILON;
console.log(Math.abs(0.1 + 0.2 - 0.3) < eps); // true== vs ===
== performs type coercion, leading to surprising results. === checks both type and value without coercion. Always use === and !==.
console.log(0 == false); // true
console.log('' == false); // true
console.log(null == undefined); // true
console.log(0 === false); // false
console.log('' === false); // false
// Always use ===Closures in Loops
var is function-scoped, so all closures share the same variable. let is block-scoped, creating a new binding per iteration. Always use let/const.
// BUG: all print 3
for (var i = 0; i < 3; i++)
setTimeout(() => console.log(i), 100);
// FIX: let
for (let i = 0; i < 3; i++)
setTimeout(() => console.log(i), 100);Async Error Handling
Unhandled promise rejections can crash Node.js. Always wrap await in try/catch, or use .catch(). Listen for unhandledRejection events.
// BUG: unhandled rejection
async function fetchData() {
const data = await fetch('/api');
return data.json();
}
// FIX: try/catch
async function fetchDataSafe() {
try { return await (await fetch('/api')).json(); }
catch (err) { console.error('Failed:', err); return null; }
}Related JavaScript snippets
Copy-paste ready code for common tasks.
Array Map Filter Reduce
Chain map, filter, reduce, find, some, and every on arrays.
Array Deduplication
Deduplicate an array using Set.
Deep Clone
Deep clone objects, supporting common data types.
Debounce Function
Wait a period of time after an event triggers before executing; reset the timer if triggered again during the wait.
Throttle Function
Limit a function to execute at most once within a time interval.
Promise.all Concurrency Control
A Promise executor with concurrency limit.
async/await Error Handling
Wrap async functions to uniformly catch exceptions.
Fetch Wrapper
Wrap fetch with timeout, error handling, and JSON parsing.
localStorage Operations
Wrap localStorage with expiration time and JSON support.
Cookie Operations
Wrap Cookie read, write, and delete operations.
URL Parameter Parsing
Parse URL query string into an object.
Date Formatting
Format a date into a specified string.
Money Formatting
Format a number as a thousands-separated money string.
Random Number Generation
Generate random numbers and random strings within a range.
Color Conversion
Convert between RGB and HEX colors.
UUID Generation
Generate a unique identifier compliant with UUID v4.
String Truncation
Truncate a string and append an ellipsis.
Array Flattening
Flatten a multi-dimensional array into one dimension.
Object Merging
Deeply merge multiple objects.
Type Checking
Precisely determine JavaScript data types.
Event Delegation
Implement event delegation via event bubbling.
DOM Manipulation
Dynamically create and manipulate DOM elements.
Form Validation
A collection of common form validation rules.
File Upload
Wrap file upload with progress and chunking support.
Image Lazy Loading
Implement image lazy loading with IntersectionObserver.
Copy to Clipboard
A cross-browser clipboard copy method.
Fullscreen API
Wrap browser fullscreen operations.
Geolocation
Get user geolocation information.
Web Worker
Create a Web Worker to run time-consuming tasks.
Service Worker
Register a Service Worker for offline caching.
IndexedDB Operations
Wrap IndexedDB CRUD operations.
Canvas Drawing
Basic Canvas drawing example.
Was this helpful?