Learn/languages/JavaScript
Beginner~20 min read

JavaScript

A thorough, modern introduction to JavaScript covering syntax, scope, functions, closures, objects, async programming, modules, and the ES2024 toolchain.

SyntaxAsyncClosuresES2024

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 /* */.

javascript
// 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.

KeywordScopeReassign?Hoisting
varFunctionYesHoisted, initialized to undefined
letBlockYesHoisted, temporal dead zone
constBlockNoHoisted, 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.

javascript
// 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.

javascript
// 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.

javascript
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.

javascript
function makeCounter() {
  let count = 0;              // private to the closure
  return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2

Objects, Destructuring & Spread

javascript
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.

javascript
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.

javascript
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

javascript
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.

QueueExamplesPriority
MicrotasksPromise.then, queueMicrotask, awaitDrained fully after each task
MacrotaskssetTimeout, setInterval, I/O eventsOne per loop iteration

Callbacks → Promises → async/await

javascript
// 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)

javascript
// 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

javascript
// 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

javascript
// 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

ToolPurpose
nodeServer-side runtime for running JS files
npm / pnpmInstall and manage dependencies
vitestFast unit test runner
eslintStatic analysis / linting
prettierOpinionated code formatting
esbuild / viteBundling and dev server
bash
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

  1. Write a makeCounter factory using a closure that also exposes a reset() method.
  2. Given an array of numbers, use filter, map, and reduce to compute the sum of the squares of the even numbers.
  3. Predict the console output of a snippet mixing setTimeout, Promise.resolve().then, and synchronous logs, then explain why using the event loop.
  4. Create a custom HttpError class and a fetchJson(url) function that throws it on non-2xx responses.
  5. Deep-clone a nested object two ways: with JSON.parse(JSON.stringify(...)) and with structuredClone. Find an input where the two differ.
  6. Split a small utility into an ES module with named exports, import it from another file, and load it dynamically with import().

Section navigation