The JavaScript Atlas
Every concept that takes you from your first console.log to writing async, prototype-savvy, production-grade code — explained, demonstrated, and ordered.
Foundations
Level 1The bedrock. These are the ideas every line of JavaScript rests on — how values are stored, how decisions are made, and how code is grouped into reusable pieces. Master these and everything later becomes pattern recognition.
What JavaScript Is
JavaScript is a high-level, interpreted programming language that runs in web browsers, on servers (via Node.js), and in countless embedded environments. It was created in 1995 to make web pages interactive and has grown into one of the most widely used languages in the world.
It is dynamically typed (you don't declare the type of a variable), single-threaded (it runs one operation at a time), and multi-paradigm (you can write it in procedural, object-oriented, or functional styles).
Your code is read and executed by an engine — V8 in Chrome and Node, SpiderMonkey in Firefox, JavaScriptCore in Safari. The engine parses your text, compiles it to fast machine instructions just-in-time, and runs it.
Running your first code
// Open any browser, press F12, click "Console", and type: console.log("Hello, world!"); // In Node.js, save as app.js and run: node app.js
Variables & Declarations
A variable is a named container for a value. JavaScript has three keywords to declare one: let, const, and the older var.
let score = 10; // can be reassigned score = 20; // fine const name = "Ada"; // cannot be reassigned // name = "Grace"; // TypeError! var old = "legacy"; // avoid in modern code
Which to use?
- Default to
const. It signals the binding won't change, which makes code easier to reason about. - Use
letonly when you genuinely need to reassign (counters, accumulators, swapping). - Avoid
var. It has confusing scope rules (function-scoped, hoisted) that cause bugs.
const on an object or array means the binding is fixed, not the contents — you can still push to a const array or change its properties.Data Types
JavaScript has seven primitive types plus objects. Primitives are immutable single values; everything else is an object.
The primitives
- String — text:
"hi",'yo',`hey` - Number — all numbers, integer or decimal:
42,3.14,-0,Infinity,NaN - BigInt — integers beyond safe number range:
9007199254740993n - Boolean —
true/false - undefined — a variable that has been declared but not assigned
- null — an intentional "no value"
- Symbol — a unique, immutable identifier (advanced)
The reference type
Object — collections of key/value pairs. Arrays, functions, dates, and almost everything else are specialized objects.
typeof "hello" // "string" typeof 42 // "number" typeof true // "boolean" typeof undefined // "undefined" typeof null // "object" ← a famous historical bug typeof {} // "object" typeof [] // "object" (use Array.isArray to check) typeof function(){} // "function"
typeof null returns "object" — a bug kept for backward compatibility. To check for null, use value === null.Operators & Type Coercion
Operators combine and compare values. The categories you'll use constantly:
// Arithmetic 5 + 2 // 7 5 % 2 // 1 (remainder / modulo) 5 ** 2 // 25 (exponent) // Comparison 5 > 2 // true 5 === "5" // false (strict: type AND value) 5 == "5" // true (loose: coerces first) // Logical true && false // false (AND) true || false // true (OR) !true // false (NOT)
Type coercion
JavaScript automatically converts types in many operations. This is powerful but surprising:
"5" + 3 // "53" (number becomes string) "5" - 3 // 2 (string becomes number) true + true // 2 (booleans become numbers) [] + [] // "" (arrays become strings)
=== and !== (strict equality). Avoid == unless you have a specific reason. This single habit prevents a whole class of bugs.Falsy values
Eight values are "falsy" — they behave like false in conditions: false, 0, -0, 0n, "", null, undefined, and NaN. Everything else is "truthy".
Control Flow
Control flow lets your program choose different paths and repeat work.
Conditionals
if (temp > 30) { console.log("Hot"); } else if (temp > 15) { console.log("Mild"); } else { console.log("Cold"); } // Ternary — a compact if/else that returns a value const label = temp > 30 ? "Hot" : "OK"; // Switch — clean multi-branch on one value switch (day) { case "Sat": case "Sun": console.log("Weekend"); break; default: console.log("Weekday"); }
break in switch cases — without it, execution "falls through" to the next case (occasionally useful, usually a bug).Loops
Loops execute a block repeatedly. JavaScript gives you several, each suited to a situation.
// Classic for — when you need an index/counter for (let i = 0; i < 5; i++) { console.log(i); // 0 1 2 3 4 } // while — repeat until a condition is false let n = 3; while (n > 0) { console.log(n); n--; } // do...while — runs the body at least once do { console.log("runs once minimum"); } while (false); // for...of — iterate VALUES of arrays/strings/iterables for (const fruit of ["apple", "pear"]) { console.log(fruit); } // for...in — iterate KEYS of an object (use sparingly) for (const key in {a:1, b:2}) { console.log(key); // "a", "b" }
Loop control: break exits the loop entirely; continue skips to the next iteration.
for...of for arrays, for...in for object keys, and the classic for when you need precise index control. For transforming arrays, prefer .map()/.filter() (covered later).Functions
A function is a reusable block of code that takes inputs (parameters), does work, and optionally returns a value. They are the fundamental unit of organization in JavaScript.
// Function declaration — hoisted, can call before definition function add(a, b) { return a + b; } // Function expression — assigned to a variable const multiply = function(a, b) { return a * b; }; // Arrow function — concise, modern (more later) const square = x => x * x; add(2, 3); // 5 square(4); // 16
Parameters & arguments
function greet(name = "friend") { // default parameter return `Hello, ${name}!`; } greet(); // "Hello, friend!" greet("Sam"); // "Hello, Sam!" // Rest parameter — collect many args into an array function sum(...nums) { return nums.reduce((a, b) => a + b, 0); } sum(1, 2, 3, 4); // 10
return statement returns undefined. The return keyword also immediately exits the function.Scope & Hoisting
Scope determines where a variable is accessible. JavaScript has global scope, function scope, and block scope.
const g = "global"; // visible everywhere function outer() { const f = "function"; // visible in outer() if (true) { const b = "block"; // visible only in this {} console.log(g, f, b); // all three work } // console.log(b); // ReferenceError }
let and const are block-scoped (confined to the nearest {}). The old var is function-scoped, ignoring blocks — a common source of bugs.
Hoisting
JavaScript "hoists" declarations to the top of their scope before running. Function declarations are fully hoisted; var is hoisted but undefined; let/const are hoisted into a "temporal dead zone" and error if used early.
sayHi(); // ✅ works — declaration hoisted function sayHi() { console.log("hi"); } // console.log(x); // ❌ ReferenceError (temporal dead zone) let x = 5;↑ Back to top
Strings
Strings are sequences of characters. They're immutable — methods return new strings rather than changing the original.
const s = "JavaScript"; s.length // 10 s.toUpperCase() // "JAVASCRIPT" s.slice(0, 4) // "Java" s.includes("Script") // true s.indexOf("S") // 4 s.replace("Java", "Type") // "TypeScript" s.split("") // ["J","a","v",...] " hi ".trim() // "hi"
Template literals
Backticks let you embed expressions and write multi-line strings — far better than + concatenation.
const name = "Sam", age = 30; const msg = `${name} is ${age} years old. This spans multiple lines.`;↑ Back to top
Numbers & Math
All JavaScript numbers are 64-bit floating point (there's no separate integer type, aside from BigInt). The Math object provides calculation helpers.
Math.round(4.6) // 5 Math.floor(4.9) // 4 Math.ceil(4.1) // 5 Math.max(1, 9, 3) // 9 Math.random() // 0 ≤ x < 1 Math.sqrt(144) // 12 (3.14159).toFixed(2) // "3.14" (string!) parseInt("42px") // 42 parseFloat("3.5kg") // 3.5 Number("123") // 123
0.1 + 0.2 === 0.3 is false (it's 0.30000000000000004). This affects all languages using IEEE 754. For money, work in integer cents or use a decimal library.// NaN = "Not a Number", the result of invalid math 0 / 0 // NaN Number.isNaN(0/0) // true — the safe way to check↑ Back to top
Arrays (Basics)
An array is an ordered, zero-indexed list of values. Arrays can hold any mix of types and grow or shrink dynamically.
const fruits = ["apple", "banana", "cherry"]; fruits[0] // "apple" (first) fruits[fruits.length - 1] // "cherry" (last) fruits.length // 3 // Adding & removing fruits.push("date"); // add to END → length 4 fruits.pop(); // remove from END fruits.unshift("kiwi"); // add to START fruits.shift(); // remove from START // Useful checks fruits.includes("banana") // true fruits.indexOf("cherry") // 2 Array.isArray(fruits) // true
const array can still be mutated — const only prevents reassigning the whole array.Objects (Basics)
An object stores data as key/value pairs (properties). It's the workhorse structure for modeling real-world things.
const person = { name: "Ada", age: 36, isAdmin: true, greet() { // a method return `Hi, I'm ${this.name}`; } }; // Two ways to access properties person.name // "Ada" (dot notation) person["age"] // 36 (bracket — for dynamic keys) person.greet() // "Hi, I'm Ada" // Add, change, delete person.email = "ada@x.com"; person.age = 37; delete person.isAdmin;
person[fieldName].DOM Basics
In the browser, the DOM (Document Object Model) is a live, tree-shaped representation of the HTML. JavaScript reads and changes it to make pages interactive.
// Selecting elements const title = document.querySelector("h1"); const all = document.querySelectorAll(".item"); const byId = document.getElementById("menu"); // Reading & changing content title.textContent = "New heading"; title.innerHTML = "<em>styled</em>"; title.style.color = "crimson"; title.classList.add("active"); title.setAttribute("data-id", "42"); // Responding to a click title.addEventListener("click", () => { console.log("Heading clicked!"); });
innerHTML with untrusted input opens you to XSS attacks. Prefer textContent for plain text; sanitize anything dynamic you inject as HTML.Debugging Basics
Every developer spends much of their time debugging. Build these habits early.
Console methods
console.log(value); // general output console.error("broke"); // red error output console.warn("careful"); // yellow warning console.table(arrayOfObjects); // neat grid console.dir(domElement); // inspect object structure console.time("t"); /* ... */ console.timeEnd("t"); // timing
The debugger
The debugger; statement pauses execution where it sits (when DevTools is open), letting you inspect variables step by step. Browser DevTools also let you set breakpoints by clicking line numbers in the Sources panel.
"x is not a function", "Cannot read properties of undefined" — each has a predictable cause you'll learn to recognize.Intermediate
Level 2Now you write JavaScript that's expressive and idiomatic. These topics — array methods, closures, the modern ES6+ toolkit, async basics — are where JavaScript starts to feel powerful instead of just functional.
Array Iteration Methods
These methods replace most manual loops with clear, expressive, chainable operations. They're among the most important things to master in JavaScript.
Transforming & filtering
const nums = [1, 2, 3, 4, 5]; // map — transform every element, returns a NEW array nums.map(n => n * 2); // [2, 4, 6, 8, 10] // filter — keep elements that pass a test nums.filter(n => n % 2 === 0); // [2, 4] // reduce — boil an array down to one value nums.reduce((sum, n) => sum + n, 0); // 15
Searching & testing
nums.find(n => n > 3); // 4 (first match) nums.findIndex(n => n > 3);// 3 nums.some(n => n > 4); // true (any pass?) nums.every(n => n > 0); // true (all pass?) nums.forEach(n => console.log(n)); // side effects only
Chaining — the real power
const result = [1,2,3,4,5,6] .filter(n => n % 2 === 0) // [2, 4, 6] .map(n => n * 10) // [20, 40, 60] .reduce((a, b) => a + b); // 120
map when you want a new array back; use forEach only for side effects (logging, DOM updates) where the return value doesn't matter.Mutating vs Non-Mutating
Some array methods change the original array (mutating); others return a new one (non-mutating). Knowing which is which prevents subtle bugs.
// MUTATING — change the original arr.push(), arr.pop(), arr.shift(), arr.unshift() arr.splice(start, count, ...items) // remove/insert arr.sort(), arr.reverse(), arr.fill() // NON-MUTATING — return a new array, leave original alone arr.slice(start, end) // extract a portion arr.concat(other) // join arrays arr.map(), arr.filter(), [...arr] // copy
sort needs care
[10, 1, 5].sort(); // [1, 10, 5] — sorts as STRINGS! // Provide a comparator for numbers: [10, 1, 5].sort((a, b) => a - b); // [1, 5, 10]
sort() and reverse() mutate in place. If you need the original intact, copy first: [...arr].sort(). Modern engines also offer non-mutating toSorted() and toReversed().Destructuring
Destructuring extracts values from arrays and objects into variables with concise syntax. You'll use it constantly.
// Array destructuring — by position const [first, second] = ["a", "b", "c"]; // first = "a", second = "b" const [head, ...tail] = [1, 2, 3, 4]; // head = 1, tail = [2, 3, 4] // Object destructuring — by key const { name, age } = person; // Rename + default values const { name: userName, role = "guest" } = person; // In function parameters — very common function draw({ x, y, color = "black" }) { /* ... */ } draw({ x: 10, y: 20 }); // Swap variables with no temp let a = 1, b = 2; [a, b] = [b, a];↑ Back to top
Spread & Rest
The three-dot ... operator does two opposite jobs depending on context: spread expands; rest collects.
Spread — expand into elements
// Copy & merge arrays const combined = [...arr1, ...arr2]; const copy = [...original]; // shallow copy // Copy & merge objects (later keys win) const merged = { ...defaults, ...overrides }; const updated = { ...user, age: 31 }; // Pass array as individual arguments Math.max(...[3, 7, 2]); // 7
Rest — collect into one
function log(first, ...others) { // others is an array of remaining args } const { id, ...rest } = bigObject; // pull off id, keep the rest
structuredClone(obj).Object Methods & Iteration
The Object constructor provides static methods for inspecting and transforming objects.
const user = { name: "Ada", age: 36, role: "dev" }; Object.keys(user) // ["name", "age", "role"] Object.values(user) // ["Ada", 36, "dev"] Object.entries(user) // [["name","Ada"], ["age",36], ...] // Iterate an object cleanly for (const [key, value] of Object.entries(user)) { console.log(`${key}: ${value}`); } // Build an object from entries (reverse of above) Object.fromEntries([["a", 1], ["b", 2]]); // {a:1, b:2} // Merge / copy Object.assign({}, target, source); // freeze to make read-only Object.freeze(config);
Shorthand & computed keys
const name = "Sam", age = 22; const obj = { name, age }; // {name:"Sam", age:22} const key = "score"; const data = { [key]: 100 }; // {score: 100}↑ Back to top
Arrow Functions
Arrow functions offer shorter syntax and — crucially — do not bind their own this. They inherit it from the surrounding scope.
// Full → concise progression const add = function(a, b) { return a + b; }; const add2 = (a, b) => { return a + b; }; const add3 = (a, b) => a + b; // implicit return const double = n => n * 2; // one param, no parens const rand = () => Math.random(); // no params // Returning an object literal needs parens const make = id => ({ id, active: true });
The this difference
const timer = { seconds: 0, start() { setInterval(() => { this.seconds++; // ✅ "this" is the timer object }, 1000); } };
this to refer to the object, or as constructors (they can't be used with new). Their lack of own this is a feature in callbacks but a trap in methods.Closures
A closure is a function that "remembers" the variables from the scope where it was created, even after that scope has finished. This is one of JavaScript's most powerful and most-asked-about concepts.
function makeCounter() { let count = 0; // private variable return function() { count++; // closes over `count` return count; }; } const counter = makeCounter(); counter(); // 1 counter(); // 2 — remembers count between calls const other = makeCounter(); other(); // 1 — independent closure
Why it matters
- Data privacy —
countcan't be accessed or tampered with from outside. - Factories — generate customized functions.
- Callbacks & event handlers rely on closures to access surrounding state.
// A configurable function factory function multiplier(factor) { return n => n * factor; // closes over `factor` } const triple = multiplier(3); triple(10); // 30↑ Back to top
The this Keyword
this refers to the object a function is operating on — but what it points to depends on how the function is called, not where it's defined. This trips up many learners.
// 1. Method call → this is the object before the dot const obj = { name: "Ada", hi() { return this.name; } }; obj.hi(); // "Ada" — this === obj // 2. Plain call → undefined (strict) or global object const fn = obj.hi; fn(); // undefined — lost its context! // 3. Arrow → inherits this from enclosing scope // 4. Constructor (new) → the new instance
Controlling this explicitly
function greet(greeting) { return greeting + " " + this.name; } const p = { name: "Sam" }; greet.call(p, "Hi"); // "Hi Sam" — call now, args listed greet.apply(p, ["Hey"]); // "Hey Sam" — call now, args array const bound = greet.bind(p); // returns a new bound function bound("Yo"); // "Yo Sam"
this manually. call and apply invoke immediately (differing only in how args are passed); bind returns a new function for later.ES6+ Essentials
ES6 (2015) and yearly updates since transformed JavaScript. Beyond let/const, arrows, destructuring, and template literals (covered already), these features are everyday tools.
// Optional chaining — safe deep access user?.address?.city // undefined instead of throwing // Nullish coalescing — default only for null/undefined const port = config.port ?? 3000; // (?? differs from || : 0 and "" are kept, not replaced) // Logical assignment x ??= 10; // assign if x is null/undefined y ||= 5; // assign if y is falsy // Array/string includes [1,2,3].includes(2); // true // Numeric separators for readability const billion = 1_000_000_000;
?? when 0, "", or false are valid values you want to keep. Use || when any falsy value should trigger the default.Modules
Modules split code across files with explicit import/export. Each module has its own scope — no more polluting the global namespace.
// ── math.js ── export const PI = 3.14159; export function add(a, b) { return a + b; } export default function multiply(a, b) { return a * b; } // ── app.js ── import multiply, { PI, add } from "./math.js"; import * as math from "./math.js"; // namespace import { add as sum } from "./math.js"; // rename
- Named exports — any number per file, imported by exact name in braces.
- Default export — one per file, imported with any name, no braces.
<script type="module" src="app.js">. Modules are deferred and run in strict mode automatically. Node.js supports them via .mjs files or "type":"module" in package.json.Error Handling
Robust programs anticipate failure. The try/catch/finally construct catches errors so your program can recover instead of crashing.
try { const data = JSON.parse(userInput); // might throw process(data); } catch (err) { console.error("Failed:", err.message); // handle / log / show friendly message } finally { // always runs — cleanup, close resources spinner.hide(); }
Throwing your own errors
function withdraw(amount, balance) { if (amount > balance) { throw new Error("Insufficient funds"); } return balance - amount; } // Custom error types class ValidationError extends Error { constructor(msg, field) { super(msg); this.name = "ValidationError"; this.field = field; } }
catch {}. At minimum log them. Catch only what you can meaningfully handle; let truly unexpected errors surface during development.DOM Events & Manipulation
Beyond basic selection, real apps create, modify, and respond to elements dynamically.
Events
button.addEventListener("click", (event) => { event.preventDefault(); // stop default behavior event.stopPropagation(); // stop bubbling up console.log(event.target); // element that fired it }); // Common events: click, input, change, submit, // keydown, mouseover, focus, blur, scroll, load
Event delegation
Instead of attaching listeners to many children, attach one to the parent and inspect event.target. Efficient and works for elements added later.
list.addEventListener("click", (e) => { if (e.target.matches("li")) { console.log("Clicked:", e.target.textContent); } });
Creating & inserting elements
const li = document.createElement("li"); li.textContent = "New item"; list.append(li); // add as last child li.remove(); // delete it↑ Back to top
JSON
JSON (JavaScript Object Notation) is the universal format for sending structured data between systems. It looks like JavaScript objects but is plain text following strict rules.
// Object → JSON string (for sending/storing) const user = { name: "Ada", roles: ["admin"], active: true }; const json = JSON.stringify(user); // '{"name":"Ada","roles":["admin"],"active":true}' // Pretty-print with indentation JSON.stringify(user, null, 2); // JSON string → object (for receiving/reading) const obj = JSON.parse(json); obj.name; // "Ada"
JSON rules
- Keys must be in double quotes
- Values: string, number, boolean, null, array, object only
- No functions, no
undefined, no comments, no trailing commas
JSON.parse throws on malformed input — always wrap it in try/catch when parsing untrusted data. JSON.stringify silently drops functions and undefined values.Timers & Async Basics
JavaScript can schedule code to run later. This is your first taste of asynchronous programming — code that doesn't run top-to-bottom.
// Run once after a delay (milliseconds) const id = setTimeout(() => { console.log("2 seconds later"); }, 2000); clearTimeout(id); // cancel before it fires // Run repeatedly const tick = setInterval(() => { console.log("every second"); }, 1000); clearInterval(tick); // stop it
Why "async"?
JavaScript is single-threaded, but timers, network requests, and user events don't block. They're handed off and their callbacks run later when the main thread is free. Understanding this leads directly to the event loop and Promises.
console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C"); // Prints: A, C, B — even with 0ms delay!↑ Back to top
Regular Expressions
Regular expressions (regex) describe text patterns for searching, validating, and replacing. Dense but immensely powerful.
const pattern = /\d+/g; // one or more digits, global "abc123def456".match(/\d+/g); // ["123", "456"] /^\d+$/.test("12345"); // true (all digits?) "hello world".replace(/o/g, "0"); // "hell0 w0rld"
Common building blocks
\ddigit,\wword char,\swhitespace,.any char*0+,+1+,?0 or 1,{2,4}range^start,$end,[abc]set,(...)group,|or- Flags:
gglobal,icase-insensitive,mmultiline
// A simple email-ish check (real validation is harder!) const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; email.test("a@b.com"); // true
Dates & Time
The built-in Date object handles dates and times — though it's notoriously awkward, so many projects reach for libraries.
const now = new Date(); // current moment const specific = new Date("2025-06-14"); const fromParts = new Date(2025, 5, 14); // month is 0-indexed! now.getFullYear(); // 2026 now.getMonth(); // 0–11 (Jan = 0) now.getDate(); // day of month 1–31 now.getDay(); // day of week 0–6 (Sun = 0) now.getTime(); // ms since Jan 1 1970 (epoch) now.toISOString(); // "2026-06-14T..." standard format now.toLocaleDateString(); // locale-aware
date-fns, Day.js, or the newer built-in Temporal API as it lands.Advanced
Level 3The concepts that separate competent from expert. Asynchronous flow, the engine's inner workings, prototypes, the functional and object-oriented depths, and the performance know-how that production code demands.
Promises
A Promise represents a value that will be available later — the result of an async operation. It's an object in one of three states: pending, fulfilled, or rejected.
const promise = new Promise((resolve, reject) => { setTimeout(() => { const ok = Math.random() > 0.5; ok ? resolve("Success!") : reject(new Error("Failed")); }, 1000); }); promise .then(result => console.log(result)) .catch(err => console.error(err)) .finally(() => console.log("done"));
Combining promises
// Wait for ALL (fails if any fails) const [a, b] = await Promise.all([fetchA(), fetchB()]); // All settle, success or failure await Promise.allSettled([p1, p2]); // First to finish wins await Promise.race([slow(), timeout()]); // First to SUCCEED await Promise.any([mirror1(), mirror2()]);
.then() returns a new promise, so you can sequence async steps without deeply nested callbacks ("callback hell").async / await
async/await is syntactic sugar over promises that lets asynchronous code read like ordinary sequential code. It's the modern default.
async function loadUser(id) { try { const res = await fetch(`/users/${id}`); if (!res.ok) throw new Error(res.status); const user = await res.json(); return user; } catch (err) { console.error("Load failed:", err); throw err; // re-throw or return a fallback } } // Calling it (also async context) const user = await loadUser(42);
- Any function marked
asyncreturns a promise automatically. awaitpauses the function until the promise settles, then resumes with its value.- Use ordinary
try/catchfor errors — much cleaner than.catch().
await a(); await b(); runs them one after another. If they're independent, run in parallel: await Promise.all([a(), b()]).The Event Loop
JavaScript is single-threaded yet handles concurrency. The event loop is the mechanism that makes this possible — essential knowledge for an expert.
The pieces
- Call stack — where functions execute, one at a time.
- Web APIs / host — timers, fetch, DOM events run outside the engine.
- Task queue (macrotasks) — callbacks from setTimeout, events, I/O.
- Microtask queue — promise callbacks, queueMicrotask, with higher priority.
The loop: run all stack work → drain all microtasks → take one macrotask → repeat. Microtasks always finish before the next macrotask.
console.log("1"); // sync setTimeout(() => console.log("2"), 0); // macrotask Promise.resolve().then(() => console.log("3")); // microtask console.log("4"); // sync // Output: 1, 4, 3, 2 // sync first, then microtask (3), then macrotask (2)
Fetch & HTTP
The fetch API makes HTTP requests to load data from and send data to servers. It returns a promise and is the modern replacement for XMLHttpRequest.
// GET request const res = await fetch("https://api.example.com/users"); const data = await res.json(); // POST with a JSON body const res2 = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Ada" }) });
Handling responses properly
async function getData(url) { const res = await fetch(url); // fetch only rejects on network failure — // you must check status yourself! if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return res.json(); }
res.ok or res.status before treating the response as success.Prototypes & Inheritance
JavaScript inheritance is prototypal: objects link to other objects (their prototype) and inherit properties through that chain. Classes are syntax sugar over this.
const animal = { speak() { return `${this.name} makes a sound`; } }; // Create an object whose prototype is `animal` const dog = Object.create(animal); dog.name = "Rex"; dog.speak(); // "Rex makes a sound" — found via prototype
The prototype chain
When you access a property, the engine looks on the object itself, then its prototype, then its prototype, up to Object.prototype and finally null. This chain is how methods like .toString() are available everywhere.
Object.getPrototypeOf(dog) === animal; // true dog.hasOwnProperty("name"); // true — own, not inherited "speak" in dog; // true — own OR inherited
.prototype object; every instance created with new links to it. This is the machinery classes hide.Classes
Classes provide clean syntax for creating objects and inheritance, built on top of prototypes.
class Animal { constructor(name) { this.name = name; // instance property } speak() { // shared via prototype return `${this.name} speaks`; } static create(name) { // called on class itself return new Animal(name); } } class Dog extends Animal { constructor(name, breed) { super(name); // call parent constructor this.breed = breed; } speak() { // override return `${this.name} barks`; } } const rex = new Dog("Rex", "Lab"); rex.speak(); // "Rex barks"
Modern class features
class Counter { #count = 0; // private field (# prefix) static label = "counter"; // static field get value() { return this.#count; } // getter increment() { this.#count++; } }
#private fields are truly inaccessible outside the class — real encapsulation, unlike the old underscore convention.Functional Programming
Functional programming (FP) emphasizes pure functions, immutability, and composition. JavaScript supports it well, and FP ideas make code more predictable and testable.
Pure functions
A pure function returns the same output for the same input and causes no side effects (no mutation, no I/O, no globals).
// Pure ✅ const add = (a, b) => a + b; // Impure ❌ (mutates external state) let total = 0; const addToTotal = n => { total += n; };
Immutability
// Don't mutate — create new values const updated = { ...user, age: 31 }; const withItem = [...list, newItem]; const withoutFirst = list.slice(1);
Composition
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x); const shout = compose( s => s + "!", s => s.toUpperCase() ); shout("hi"); // "HI!"
map, filter, reduce, and your own factories all qualify.Currying & Memoization
Several powerful patterns build directly on closures.
Currying
Transform a multi-argument function into a chain of single-argument functions — useful for specialization and composition.
const curry = fn => a => b => c => fn(a, b, c); const volume = curry((l, w, h) => l * w * h); volume(2)(3)(4); // 24 // Partial application — fix some args const greet = greeting => name => `${greeting}, ${name}`; const sayHi = greet("Hi"); sayHi("Ada"); // "Hi, Ada"
Memoization
Cache a function's results so repeated calls with the same input are instant.
function memoize(fn) { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn(...args); cache.set(key, result); return result; }; } const fastFib = memoize(fib);↑ Back to top
Iterators & Generators
Iterators define how an object is looped over. Generators are special functions that produce values lazily, pausing and resuming.
Generators
function* range(start, end) { for (let i = start; i <= end; i++) { yield i; // pause and emit a value } } for (const n of range(1, 4)) console.log(n); // 1 2 3 4 [...range(1, 3)]; // [1, 2, 3] // Infinite sequences — lazy, only computes when asked function* ids() { let i = 1; while (true) yield i++; } const gen = ids(); gen.next().value; // 1 gen.next().value; // 2
Custom iterables
const collection = { items: ["a", "b", "c"], [Symbol.iterator]() { let i = 0; return { next: () => i < this.items.length ? { value: this.items[i++], done: false } : { value: undefined, done: true } }; } }; [...collection]; // ["a", "b", "c"]↑ Back to top
Map, Set, WeakMap, WeakSet
Beyond objects and arrays, ES6 added specialized collection types with distinct advantages.
Map — keyed collection
const map = new Map(); map.set("name", "Ada"); map.set(obj, "any value as key!"); // keys can be ANY type map.get("name"); // "Ada" map.has("name"); // true map.size; // 2 for (const [k, v] of map) { /* iterates in insertion order */ }
Map vs Object: Maps allow any key type, preserve insertion order, have a .size, and iterate easily — better for dynamic key/value collections.
Set — unique values
const set = new Set([1, 2, 2, 3, 3]); set.size; // 3 — duplicates removed set.add(4); set.has(2); // true // Deduplicate an array instantly const unique = [...new Set(arr)];
WeakMap / WeakSet
Hold object keys "weakly" — if no other reference exists, they can be garbage collected. Used for private data and caches without memory leaks. Not iterable and keys must be objects.
↑ Back to topSymbols
A Symbol is a unique, immutable primitive often used as a non-colliding object key or to define special behaviors.
const id = Symbol("id"); // the string is just a label Symbol("id") === Symbol("id"); // false — always unique const user = { [id]: 123, name: "Ada" }; user[id]; // 123 — won't clash with any "id" string key
Well-known symbols
Symbols also let you hook into language behaviors:
const obj = { [Symbol.iterator]() { /* makes obj iterable */ }, [Symbol.toPrimitive](hint) { /* custom coercion */ } }; // Global symbol registry (shared across realms) const shared = Symbol.for("app.id"); Symbol.for("app.id") === shared; // true
for...in, Object.keys, and JSON — making them useful for "hidden" metadata.Proxy & Reflect
Proxy wraps an object to intercept and customize fundamental operations (get, set, delete, etc.). Reflect provides the default versions of those operations.
const target = { name: "Ada", age: 36 }; const proxy = new Proxy(target, { get(obj, prop) { console.log(`reading ${prop}`); return Reflect.get(obj, prop); }, set(obj, prop, value) { if (prop === "age" && typeof value !== "number") { throw new TypeError("age must be a number"); } return Reflect.set(obj, prop, value); } }); proxy.name; // logs "reading name" → "Ada" proxy.age = "old"; // throws TypeError
Module Systems Deep-Dive
JavaScript has two major module systems. Understanding both matters when working across browser and Node.js code.
ES Modules (ESM) — the standard
import { thing } from "./mod.js"; export const x = 1; // Static, hoisted, async-loaded, tree-shakeable // Works in browsers and modern Node
CommonJS (CJS) — Node's legacy
const thing = require("./mod"); module.exports = { x: 1 }; // Synchronous, dynamic, Node-only origin
Dynamic imports
Load modules on demand — for code splitting and lazy loading.
async function loadChart() { const { Chart } = await import("./chart.js"); return new Chart(); } // Only fetches chart.js when actually needed
Memory & Garbage Collection
JavaScript manages memory automatically via garbage collection (GC). The engine periodically frees memory occupied by values that are no longer reachable. Understanding this prevents leaks.
How reachability works
A value is "reachable" if it can be accessed from a root (global object, current call stack, etc.) through a chain of references. Once nothing references it, it becomes eligible for collection.
Common leak sources
- Forgotten timers/listeners —
setIntervalnever cleared, listeners never removed, keep their closures alive. - Detached DOM nodes — JS variables holding removed elements.
- Growing caches — Maps/objects that only ever add entries.
- Closures capturing large objects unnecessarily.
// Leak: listener keeps `bigData` alive forever element.addEventListener("click", () => use(bigData)); // Fix: remove when done const handler = () => use(bigData); element.addEventListener("click", handler); // later: element.removeEventListener("click", handler);
WeakMap/WeakSet for caches keyed by objects so entries vanish when the objects do. Browser DevTools' Memory panel finds leaks via heap snapshots.Performance Patterns
Smooth, responsive apps require controlling how often expensive work runs.
Debounce — wait for a pause
Run a function only after activity stops. Ideal for search-as-you-type and resize handlers.
function debounce(fn, delay) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } input.addEventListener("input", debounce(search, 300));
Throttle — limit the rate
Run at most once per interval. Ideal for scroll and mousemove handlers.
function throttle(fn, limit) { let waiting = false; return (...args) => { if (!waiting) { fn(...args); waiting = true; setTimeout(() => waiting = false, limit); } }; }
Other techniques
- requestAnimationFrame for visual updates synced to the display refresh.
- Batch DOM reads/writes to avoid layout thrashing.
- Virtualize long lists — render only visible rows.
- Lazy-load images and modules.
Browser Web APIs
Browsers expose many APIs beyond the DOM for storage, background work, and device features.
Storage
// localStorage — persists across sessions, ~5–10MB, strings only localStorage.setItem("theme", "dark"); localStorage.getItem("theme"); // "dark" localStorage.removeItem("theme"); // sessionStorage — same API, cleared when tab closes // Store objects via JSON localStorage.setItem("user", JSON.stringify(user));
IndexedDB
A full in-browser database for large, structured data and offline apps (asynchronous, more complex — often used via wrappers like idb).
Web Workers
Run heavy computation on a background thread so the UI stays responsive.
// main.js const worker = new Worker("worker.js"); worker.postMessage({ numbers: bigArray }); worker.onmessage = e => console.log(e.data); // worker.js onmessage = e => { const result = heavyCompute(e.data.numbers); postMessage(result); };
Design Patterns
Design patterns are proven templates for common problems. A few that appear constantly in JavaScript:
Module pattern
const counter = (() => { let count = 0; // private return { increment() { return ++count; }, get() { return count; } }; })(); // IIFE creates one instance with privacy
Observer (pub/sub)
class Emitter { #listeners = {}; on(event, fn) { (this.#listeners[event] ??= []).push(fn); } emit(event, data) { (this.#listeners[event] ?? []).forEach(fn => fn(data)); } }
Factory & Singleton
// Factory — function that builds objects const createUser = (name, role) => ({ name, role, created: Date.now() }); // Singleton — one shared instance const Config = (() => { let instance; return { get() { return instance ??= { loaded: true }; } }; })();
Ecosystem & Beyond
Level 4JavaScript rarely runs alone. This final level surveys the surrounding world — the language layer on top (TypeScript), the tools that build and test your code, the runtimes and frameworks, and the security knowledge every professional needs.
TypeScript
TypeScript is a superset of JavaScript that adds static types, checked before your code runs. It catches whole categories of bugs at write-time and powers excellent editor autocomplete.
// Annotate variables, params, and returns function greet(name: string): string { return `Hi, ${name}`; } // Interfaces & types describe object shapes interface User { id: number; name: string; email?: string; // optional readonly role: string; } // Generics — reusable, type-safe abstractions function first<T>(arr: T[]): T | undefined { return arr[0]; } // Union & literal types type Status = "idle" | "loading" | "error";
Build Tools & Bundlers
Modern apps use build tools to bundle modules, transpile new syntax for older browsers, and optimize output.
- Vite — the current default: instant dev server (native ESM), fast production builds via Rollup. Great DX.
- esbuild — extremely fast bundler/transpiler written in Go; powers many other tools.
- Webpack — the long-dominant, highly configurable bundler; still common in established projects.
- Rollup — excellent for libraries; pioneered tree-shaking.
- Babel — transpiles modern/experimental JS to widely supported versions.
What they do
- Bundling — combine many modules into few optimized files.
- Transpiling — convert TS/JSX/new syntax to compatible JS.
- Minification — strip whitespace and shorten names.
- Tree-shaking — remove unused code.
- Code splitting — load parts of the app on demand.
npm create vite@latest. You rarely configure bundlers by hand anymore — sensible defaults cover most needs.Package Management
npm (Node Package Manager) installs and manages the libraries your project depends on, drawing from a registry of millions of packages.
# Initialize a project npm init -y # Install dependencies npm install lodash # runtime dependency npm install -D vitest # dev-only dependency npm install # install everything in package.json # Run scripts defined in package.json npm run build npm test
Key files
- package.json — project metadata, dependencies, and scripts.
- package-lock.json — exact resolved versions for reproducible installs.
- node_modules/ — the installed packages (never commit this).
Semantic versioning (^1.2.3): MAJOR.MINOR.PATCH. ^ allows minor/patch updates; ~ allows patch only.
Testing
Automated tests verify your code works and keep it working as you change it. They're essential for any non-trivial project.
Test types
- Unit — test one function/module in isolation.
- Integration — test pieces working together.
- End-to-end (E2E) — test full user flows in a real browser.
// A unit test with Vitest / Jest import { describe, it, expect } from "vitest"; import { add } from "./math.js"; describe("add", () => { it("sums two numbers", () => { expect(add(2, 3)).toBe(5); }); it("handles negatives", () => { expect(add(-1, 1)).toBe(0); }); });
The toolkit
- Vitest / Jest — test runners and assertion libraries.
- Testing Library — test UI the way users interact with it.
- Playwright / Cypress — E2E browser automation.
Node.js
Node.js runs JavaScript outside the browser, powered by Chrome's V8 engine. It's used for servers, APIs, command-line tools, and build tooling.
// A minimal HTTP server (no framework) import { createServer } from "node:http"; createServer((req, res) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello from Node" })); }).listen(3000);
The ecosystem
- Built-in modules —
fs(files),path,http,crypto,os, and more. - Express / Fastify / Hono — web frameworks for building APIs.
- Event-driven, non-blocking I/O — handles many connections efficiently on one thread.
Frontend Frameworks
Frameworks manage UI complexity — state, rendering, and components — so you don't hand-write DOM updates. The major players:
- React — the most widely used. Component-based, uses a virtual DOM, vast ecosystem. Pairs with Next.js for full-stack apps.
- Vue — approachable, gentle learning curve, excellent docs. Nuxt is its full-stack meta-framework.
- Angular — full, opinionated framework with TypeScript and tooling built in; common in enterprises.
- Svelte — compiles components to minimal vanilla JS at build time; no runtime virtual DOM. SvelteKit for full apps.
- Solid — fine-grained reactivity with React-like syntax and exceptional performance.
// A React component — the dominant mental model function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> Clicked {count} times </button> ); }
State Management
As apps grow, sharing and updating data across many components becomes hard. State management tools solve this.
- Component state — local data (React's useState, Vue's ref). The starting point.
- Context / provide-inject — pass data down a tree without prop-drilling.
- Stores — centralized, shared state: Redux/Redux Toolkit, Zustand, Jotai (React); Pinia (Vue); Svelte stores.
- Server state — TanStack Query, SWR: cache, sync, and revalidate data from APIs.
Core idea
Most modern state tools follow one principle: state is a single source of truth, updated through controlled actions, and the UI reactively re-renders when it changes. This predictable one-way flow makes complex apps debuggable.
Web Security
Every JavaScript developer must understand common vulnerabilities and their defenses. Security is not optional.
XSS — Cross-Site Scripting
Attackers inject malicious scripts via unsanitized user input. Defense: never insert untrusted data as HTML; use textContent, sanitize with libraries like DOMPurify, and set a Content Security Policy.
// ❌ Dangerous el.innerHTML = userInput; // ✅ Safe el.textContent = userInput;
CSRF — Cross-Site Request Forgery
Tricks a logged-in user's browser into making unwanted requests. Defense: anti-CSRF tokens, SameSite cookies.
CORS — Cross-Origin Resource Sharing
A browser security model controlling which origins may call your API. Servers grant access via response headers; it's a protection, not an obstacle to route around.
General principles
- Validate and sanitize all input — on the server, always.
- Never trust the client; never put secrets/API keys in front-end code.
- Use HTTPS everywhere; store tokens carefully (httpOnly cookies over localStorage for sensitive auth).
- Keep dependencies updated — run
npm audit.
PWAs, WebAssembly & What's Next
A few advanced platform capabilities round out an expert's awareness.
Progressive Web Apps (PWAs)
Web apps that behave like native ones: installable, offline-capable (via service workers), with push notifications. Built from a manifest file plus a service worker caching strategy.
WebAssembly (Wasm)
A binary format letting languages like Rust, C++, and Go run in the browser at near-native speed. JavaScript and Wasm interoperate — use Wasm for compute-heavy tasks (image/video processing, games, simulations) and JS for everything else.
// Loading a Wasm module from JS const wasm = await WebAssembly.instantiateStreaming( fetch("module.wasm") ); wasm.instance.exports.compute(42);
Keeping current
- TC39 proposals shape the language yearly — watch stage-3/4 features (Temporal, decorators, pattern matching).
- MDN Web Docs is the authoritative reference for every API.
- Read source code of libraries you use — the fastest way to deepen mastery.