JavaScript is the language of the web and, thanks to Node.js and other runtimes, of servers, tooling, and desktop apps too. It is dynamically typed, single-threaded, and event-driven. These notes cover the modern language as of the ES2024 specification, plus the tooling you will meet in real projects.
Syntax Basics
Statements end with an optional semicolon (automatic semicolon insertion handles most cases, but explicit semicolons are safer). Comments use // and /* */.
// A first program
const name = "world";
console.log(`Hello, ${name}!`); // template literal
// Multiple values
let count = 0;
count += 1;
Variables: var, let, const
Prefer const by default and let when you must reassign. Avoid var in new code: it is function-scoped and hoisted in confusing ways.
| Keyword | Scope | Reassign? | Hoisting |
|---|---|---|---|
var | Function | Yes | Hoisted, initialized to undefined |
let | Block | Yes | Hoisted, temporal dead zone |
const | Block | No | Hoisted, temporal dead zone |
Temporal Dead Zone
A let/const binding exists from the start of its block but cannot be accessed until the declaration line. Touching it earlier throws a ReferenceError.
Types: Primitive vs Reference
JavaScript has seven primitives: string, number, boolean, null, undefined, bigint, and symbol. Everything else (objects, arrays, functions) is a reference type.
// Primitives copy by value
let a = 10;
let b = a;
b = 20;
console.log(a); // 10
// Objects copy by reference
const o1 = { x: 1 };
const o2 = o1;
o2.x = 99;
console.log(o1.x); // 99 — same object
== vs ===
Always prefer === (strict equality, no coercion). == coerces types, so 0 == "" and null == undefined are both true, which surprises people.
Functions
Functions are first-class values: you can pass them around, return them, and store them. There are declarations, expressions, and arrow functions.
// Declaration (hoisted)
function add(a, b) { return a + b; }
// Expression
const sub = function (a, b) { return a - b; };
// Arrow function (concise, lexical this)
const mul = (a, b) => a * b;
// Default and rest parameters
function greet(name = "friend", ...others) {
return [name, ...others].join(", ");
}
Arrow functions and this
Arrow functions do not have their own this; they capture it from the enclosing scope. Regular functions bind this based on how they are called.
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // arrow keeps `this` = timer
}, 1000);
},
};
Closures
A closure is a function that remembers the variables from the scope where it was created, even after that scope has returned. Closures power private state, memoization, and factories.
function makeCounter() {
let count = 0; // private to the closure
return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2
Objects, Destructuring & Spread
const user = { id: 1, name: "Ada", role: "admin" };
// Destructuring with rename and default
const { name, role = "guest" } = user;
// Spread to copy/merge
const patched = { ...user, role: "owner" };
// Computed keys and shorthand
const key = "score";
const stats = { [key]: 42, name };
Destructuring works on arrays too, and can be nested. The spread operator (...) shallow-copies enumerable properties.
Arrays & Higher-Order Methods
Arrays offer functional methods that return new arrays (immutable style) rather than mutating in place.
const nums = [1, 2, 3, 4, 5];
nums.map(n => n * 2); // [2,4,6,8,10]
nums.filter(n => n % 2 === 0); // [2,4]
nums.reduce((sum, n) => sum + n, 0); // 15
nums.find(n => n > 3); // 4
nums.some(n => n > 4); // true
nums.every(n => n > 0); // true
// ES2023+ non-mutating variants
nums.toSorted((a, b) => b - a); // new sorted array
nums.toReversed(); // new reversed array
nums.findLast(n => n < 4); // 3
Prototypes & Classes
Objects inherit from a prototype chain. The class syntax is sugar over prototypes, with fields, private members (#), getters, and static members.
class Animal {
#energy = 100; // private field
constructor(name) { this.name = name; }
eat(amount) { this.#energy += amount; }
get energy() { return this.#energy; }
static create(name) { return new Animal(name); }
}
class Dog extends Animal {
speak() { return `${this.name} says woof`; }
}
const d = new Dog("Rex");
d.speak(); // "Rex says woof"
d instanceof Animal; // true
Error Handling
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
try {
throw new ValidationError("Email required", "email");
} catch (err) {
if (err instanceof ValidationError) {
console.error(err.field, err.message);
}
} finally {
console.log("always runs");
}
Error.cause
Wrap lower-level errors while preserving context: throw new Error("Failed", { cause: original }). Inspect it later with err.cause.
The Event Loop & Async
JavaScript runs on a single thread. Long-running work is offloaded and its results are queued as tasks. The event loop processes the call stack, then microtasks (promises), then a macrotask (timers, I/O), repeating.
| Queue | Examples | Priority |
|---|---|---|
| Microtasks | Promise.then, queueMicrotask, await | Drained fully after each task |
| Macrotasks | setTimeout, setInterval, I/O events | One per loop iteration |
Callbacks → Promises → async/await
// Promise
function load(url) {
return fetch(url).then(res => res.json());
}
// async/await (reads top-to-bottom)
async function main() {
try {
const data = await load("/api/users");
console.log(data);
} catch (err) {
console.error("Request failed", err);
}
}
// Run in parallel
const [a, b] = await Promise.all([load("/a"), load("/b")]);
// First to settle, without throwing on rejection
const results = await Promise.allSettled([load("/a"), load("/b")]);
Modules (ESM)
// math.js — named + default exports
export const PI = 3.14159;
export function square(x) { return x * x; }
export default function cube(x) { return x ** 3; }
// app.js
import cube, { PI, square } from "./math.js";
import * as math from "./math.js";
// Dynamic import (returns a promise)
const mod = await import("./math.js");
Top-level await
Inside an ES module you can use await at the top level without an async wrapper. The module simply waits before its exports resolve.
Modern Features
// Optional chaining — short-circuits on null/undefined
const city = user?.address?.city;
// Nullish coalescing — only null/undefined fall through
const page = input ?? 1; // 0 and "" are kept
// Logical assignment
config.timeout ??= 3000;
count ||= 1;
// ES2024: group array items into an object
const byParity = Object.groupBy([1, 2, 3, 4], n =>
n % 2 ? "odd" : "even");
Standard Library & Runtime
// Map & Set
const seen = new Set([1, 2, 2, 3]); // {1,2,3}
const scores = new Map([["ada", 90]]);
scores.set("bob", 75).get("ada"); // 90
// JSON
const text = JSON.stringify({ ok: true });
const obj = JSON.parse(text);
// Deep clone (structured clone algorithm)
const copy = structuredClone({ nested: { a: [1, 2] } });
// fetch is built into browsers and modern Node
const res = await fetch("https://api.example.com/data");
const json = await res.json();
Tooling
| Tool | Purpose |
|---|---|
node | Server-side runtime for running JS files |
npm / pnpm | Install and manage dependencies |
vitest | Fast unit test runner |
eslint | Static analysis / linting |
prettier | Opinionated code formatting |
esbuild / vite | Bundling and dev server |
npm init -y # create package.json
npm install lodash # add a dependency
npx vitest # run tests
npx eslint . # lint the project
npx prettier --write . # format all files
node app.js # run a script
Practice Exercises
- Write a
makeCounterfactory using a closure that also exposes areset()method. - Given an array of numbers, use
filter,map, andreduceto compute the sum of the squares of the even numbers. - Predict the console output of a snippet mixing
setTimeout,Promise.resolve().then, and synchronous logs, then explain why using the event loop. - Create a custom
HttpErrorclass and afetchJson(url)function that throws it on non-2xx responses. - Deep-clone a nested object two ways: with
JSON.parse(JSON.stringify(...))and withstructuredClone. Find an input where the two differ. - Split a small utility into an ES module with named exports, import it from another file, and load it dynamically with
import().