Back to JavaScript Examples

Basic JavaScript

A quick-reference and hands-on guide to the fundamentals of JavaScript programming.

How to use this page

The fastest way to keep what you learn is to teach it. The physicist Richard Feynman's trick was simple: if you can't explain something in plain words, you don't really understand it yet. For each section, work these four steps:

  1. Read the lesson once.
  2. Explain it out loud in plain words, as if teaching a friend — no jargon. If you stall, that's the gap.
  3. Do the exercise from a blank editor, without peeking. Where you get stuck is exactly what to reread.
  4. Reveal the solution, compare, and explain the idea again in your own words.

Every exercise is a complete program. Most are plain JavaScript: save as practice.js and run node practice.js, or paste into your browser's Console (press F12). The DOM and Events exercises (sections 10 and 11) are small web pages: save as practice.html, open it in a browser, and look at the Console. Watch for the 💡 Explain it simply prompts and ✎ Exercise boxes in each section. Hover any code block and click Copy to grab it.

Contents

  1. What is JavaScript?
  2. Variables & Data Types
  3. Operators
  4. Strings
  5. Arrays
  6. Objects
  7. Control Structures
  8. Loops
  9. Functions
  10. DOM Manipulation
  11. Events
  12. Async JavaScript

1. What is JavaScript?

A programming language is a precise, written notation for telling a computer what to do. JavaScript was created by Brendan Eich at Netscape in 1995, famously in about ten days, to make web pages interactive. Despite the name, it has nothing to do with Java; the name was a marketing decision. Today it is the only programming language that every web browser runs natively, which makes it the most widely deployed language in the world. The language itself is standardised as ECMAScript, and the big 2015 update (ES6) added much of the modern syntax used on this page: let, const, arrow functions, classes, and template literals.

JavaScript runs in two main places. In the browser, it's loaded by a <script> tag and can read and change the page, respond to clicks and typing, and talk to servers. Outside the browser, Node.js runs JavaScript on servers and on your own computer, which is how many web back ends and developer tools are built. The core language is the same in both; what differs is the environment's built-in objects. The browser provides document and window, while Node provides file and network access.

JavaScript is dynamically typed: variables don't declare a type, and values can change type. It is also interpreted from the programmer's point of view: you give the engine source code and it runs it, although modern engines such as Chrome's V8 compile hot code to machine code on the fly for speed. And it is single-threaded with an event loop: it does one thing at a time, but it never sits waiting for slow work like a network request. It carries on and comes back when the result is ready, as section 12 explains.

ECMAScript
The official standard that defines the JavaScript language; ES6 (2015) was the major modernisation.
Node.js
A program that runs JavaScript outside the browser: on servers and on your own computer.
Runtime environment
What surrounds the language: the browser gives you document, while Node gives you files and servers.
// Inline in HTML
<script>
  console.log("Hello, World!");
</script>

// External file
<script src="app.js"></script>
Explain it simply

JavaScript runs in the browser and on servers. What's the same in both places, and what's different? Explain it with a car engine.

Reveal a plain-language answer

The language is the engine: the same rules for variables, loops, and functions everywhere. The browser and Node.js are two different vehicles built around that engine. The browser version comes with a steering wheel for web pages (document, buttons, clicks), and the Node version comes with a loading bay for files and network servers. You drive them the same way; they just have different controls.

2. Variables & Data Types

A variable is a name that refers to a value. Modern JavaScript gives you two keywords for creating one. const creates a binding that can't be reassigned, and let creates one that can. The older var still works but has confusing scoping rules, so modern code avoids it. The standard advice is to use const by default and switch to let only when you know the value must change, so a reader can see at a glance which names stay fixed. Note that const fixes the binding, not the contents: a const array can still have items pushed onto it.

JavaScript has seven primitive types. string is text, and number is any number: JavaScript has one number type for both whole numbers and decimals, stored as a 64-bit float. boolean is true or false. There are also bigint for arbitrarily large integers and symbol for unique identifiers. Two values mean “nothing”: undefined is what a variable holds before it's been given a value, and null is what you assign deliberately to mean “no value”. Everything else, including arrays and functions, is an object. The typeof operator reports a value's type as a string.

Variables declared with let and const have block scope: they exist only inside the nearest pair of curly braces, such as a loop body or an if block. Because JavaScript is dynamically typed, the same let variable could hold a number now and a string later, but doing so makes code harder to follow. Names are case-sensitive and conventionally written in camelCase.

const / let
A binding that can't be reassigned / one that can; prefer const.
undefined vs null
“Not assigned yet” / “deliberately empty”.
Block scope
A let/const variable exists only inside its { } block.
const name    = "Alice";      // String  — cannot be reassigned
let   age     = 30;           // Number  — can be reassigned
let   price   = 9.99;         // Number (JS has one number type)
const active  = true;         // Boolean
let   nothing = null;         // Null (intentional absence)
let   undef;                  // undefined (declared, not assigned)

console.log(typeof name);    // "string"
console.log(typeof age);     // "number"
console.log(typeof active);  // "boolean"
TypeExampleDescription
string"hello"Text
number42, 3.14All numbers (int and float)
booleantrue / falseBoolean values
nullnullIntentional empty value
undefinedundefinedVariable declared but not set
object{ key: val }Key-value pairs
array[1, 2, 3]Ordered list (technically an object)
Exercise — your turn

Save as practice.js and run node practice.js (or paste into your browser's console). Declare name and age with the right keywords, since one of them changes, then finish the typeof lines.

// TODO: declare name ("Ada") and age (36), choosing const or let for each
console.log(`${name} is ${age} years old.`);

age = age + 1;
console.log(`Next year: ${age}`);

let missing;
console.log(`typeof age: ${""}`);       // TODO: typeof age
console.log(`typeof name: ${""}`);      // TODO: typeof name
console.log(`typeof missing: ${""}`);   // TODO: typeof missing
Show solution
const name = "Ada";
let age = 36;
console.log(`${name} is ${age} years old.`);

age = age + 1;
console.log(`Next year: ${age}`);

let missing;
console.log(`typeof age: ${typeof age}`);
console.log(`typeof name: ${typeof name}`);
console.log(`typeof missing: ${typeof missing}`);
Output
Ada is 36 years old.
Next year: 37
typeof age: number
typeof name: string
typeof missing: undefined
Explain it simply

Why is const the recommended default, when let can do everything const can?

Reveal a plain-language answer

Because const is a promise to the reader: “this name will always mean this value.” If most names are const, the few lets stand out as the only things that change, which is where bugs tend to hide. It's like marking only the dials on a machine that can move; everything unmarked is bolted down.

3. Operators

An operator is a symbol that performs an operation on one or more values, its operands. JavaScript's arithmetic operators are + - * / %, plus ** for exponentiation and ++/-- to add or subtract one. Because there's only one number type, / always gives the exact result, so 17 / 5 is 3.4; use Math.floor or Math.trunc when you want a whole number. Numbers are binary floating point, so some decimals can't be stored exactly: 0.1 + 0.2 gives 0.30000000000000004. Round money with toFixed, or work in whole cents.

The + operator does double duty: it adds numbers but joins strings, and if either side is a string, the other side is converted to text. So "5" + 3 is "53", while "5" - 3 is 2, because - only makes sense for numbers. This automatic type coercion is JavaScript's most famous trap, and it matters most for equality. The loose operator == coerces before comparing, so "5" == 5 is true. The strict operator === requires the same type and value, so "5" === 5 is false. Always use === and !==.

The logical operators &&, ||, and ! work with any values, not just booleans, using truthiness. false, 0, "", null, undefined, and NaN are falsy, and everything else is truthy. a || b returns a if it's truthy, otherwise b, which is why it was traditionally used for defaults. The newer nullish coalescing operator ?? is usually better for defaults, because it falls back only on null or undefined and keeps legitimate values like 0 and "".

Type coercion
JavaScript automatically converting a value's type, e.g. "5" + 3 becoming "53".
=== vs ==
Strict equality (same type and value) / loose equality (converts first); prefer ===.
Truthy / falsy
How a value behaves in a condition; 0, "", null, undefined, NaN are falsy.

Arithmetic

const a = 10, b = 3;
a + b;    // 13 — addition
a - b;    // 7  — subtraction
a * b;    // 30 — multiplication
a / b;    // 3.333… — division
a % b;    // 1  — modulus (remainder)
a ** b;   // 1000 — exponentiation

Comparison

x === y   // Strict equal (value AND type) ✓ always use this
x !== y   // Strict not equal
x ==  y   // Loose equal (type coercion — avoid)
x <   y   // Less than
x >   y   // Greater than
x <=  y   // Less than or equal
x >=  y   // Greater than or equal
Always use === instead of ==. Loose equality causes surprising results: "5" == 5 is true, but "5" === 5 is false.

Logical

a && b   // AND
a || b   // OR
!a        // NOT
a ?? b   // Nullish coalescing — use b if a is null/undefined
Exercise — your turn

A carton holds 5 eggs. Replace each 0, then predict the last three lines before you run them: string joining, loose vs. strict equality, and floating point.

const eggs = 17;
const perBox = 5;

console.log(`Full boxes: ${0}`);   // TODO: Math.floor of the division
console.log(`Left over: ${0}`);    // TODO: %
console.log(`Exact: ${0}`);        // TODO: /
console.log(`2 ** 10 = ${0}`);     // TODO: **
console.log(`"5" + 3 = ${"5" + 3}`);
console.log(`"5" == 5: ${"5" == 5}, "5" === 5: ${"5" === 5}`);
console.log(`0.1 + 0.2 = ${0.1 + 0.2}`);
Show solution
const eggs = 17;
const perBox = 5;

console.log(`Full boxes: ${Math.floor(eggs / perBox)}`);
console.log(`Left over: ${eggs % perBox}`);
console.log(`Exact: ${eggs / perBox}`);
console.log(`2 ** 10 = ${2 ** 10}`);
console.log(`"5" + 3 = ${"5" + 3}`);
console.log(`"5" == 5: ${"5" == 5}, "5" === 5: ${"5" === 5}`);
console.log(`0.1 + 0.2 = ${0.1 + 0.2}`);
Output
Full boxes: 3
Left over: 2
Exact: 3.4
2 ** 10 = 1024
"5" + 3 = 53
"5" == 5: true, "5" === 5: false
0.1 + 0.2 = 0.30000000000000004
Explain it simply

Why is "5" + 3 equal to "53" but "5" - 3 equal to 2?

Reveal a plain-language answer

+ has two jobs: adding numbers and gluing text together. When one side is text, JavaScript picks gluing, so it turns 3 into "3" and sticks them together. - has only one job, subtraction, so JavaScript turns "5" into the number 5 and subtracts. Same values, different operator, different guess. That's why careful code converts types explicitly instead of letting JavaScript guess.

4. Strings

A string is a sequence of characters used for text. JavaScript accepts single quotes, double quotes, or backticks. Backticks create a template literal, the modern way to build text: it can span several lines, and any expression inside ${ } is evaluated and inserted, as in `Hello, ${name}!`. Template literals replace most uses of joining strings with + and are much easier to read.

Strings are immutable: no method changes a string in place. Methods such as toUpperCase(), trim(), replace(), and slice() all return a new string, so you must use or store the result. A string has a length property, and individual characters can be read by index, starting from 0: s[0] or s.charAt(0). s.at(-1) reads the last character.

The workhorse methods are includes, startsWith, and indexOf for searching, slice(start, end) for extracting part of a string (the end is excluded), split(separator) for breaking a string into an array, and the array method join(separator) for gluing one back together. padStart and padEnd add spaces (or other characters) to reach a fixed width, which is handy for aligning columns. Numbers have toFixed(2) for formatting with exactly two decimal places.

Template literal
A backtick string where ${expression} is evaluated and inserted.
Immutable
Cannot be changed in place; string methods return new strings.
split / join
Break a string into an array / glue an array back into a string.
const first = "Alice";
const last  = "Smith";

// Concatenation
first + " " + last;            // "Alice Smith"

// Template literals (backticks) — preferred
`Hello, ${first}!`;              // "Hello, Alice!"
`${first} is ${2026 - 1996} years old.`;

// Common string methods
"hello".length;                 // 5
"hello".toUpperCase();          // "HELLO"
"hello".toLowerCase();          // "hello"
"  hello  ".trim();            // "hello"
"hello".includes("ell");        // true
"hello".startsWith("he");      // true
"hello".indexOf("l");          // 2
"hello".slice(1, 4);           // "ell"
"hello".replace("l", "r");     // "herlo"
"a,b,c".split(",");            // ["a","b","c"]
["a","b"].join("-");           // "a-b"
Exercise — your turn

Clean up the messy name: trim, split into words, capitalise each word with map, and join. Build the initials, then print a padded receipt line using padEnd, toFixed, and padStart.

const raw = "  ada lovelace  ";

const words = raw.trim().split(" ");
const capitalised = words;   // TODO: map each word to first letter upper-cased + the rest
const full = "";             // TODO: join the capitalised words with a space
const initials = "";         // TODO: first letter of each word plus "." (hint: map, then join(""))

console.log(`${full} (${full.length} chars)`);
console.log(`Initials: ${initials}`);
// TODO: print "Coffee" padded to 8 wide, then 3.5 as "3.50" padded to 6 wide, between | marks
Show solution
const raw = "  ada lovelace  ";

const words = raw.trim().split(" ");
const capitalised = words.map(w => w.charAt(0).toUpperCase() + w.slice(1));
const full = capitalised.join(" ");
const initials = capitalised.map(w => w[0] + ".").join("");

console.log(`${full} (${full.length} chars)`);
console.log(`Initials: ${initials}`);
console.log(`${"Coffee".padEnd(8)}|${(3.5).toFixed(2).padStart(6)}|`);
Output
Ada Lovelace (12 chars)
Initials: A.L.
Coffee  |  3.50|
Explain it simply

Why do people prefer backtick template literals over joining strings with +?

Reveal a plain-language answer

With + you're assembling a sentence out of fragments, and it's easy to lose a space or a quote: "Hi " + name + ", you are " + age. A template literal is the finished sentence with blanks in it, `Hi ${name}, you are ${age}`, so you can see exactly what the output will look like. It's a fill-in-the-blanks form instead of a ransom note.

5. Arrays

An array is an ordered list of values, written in square brackets: ["apple", "banana"]. Items are reached by index, starting at 0, and length gives the count. Arrays grow and shrink freely: push adds to the end, pop removes from the end, unshift and shift do the same at the front, and splice inserts or removes in the middle. These methods change the array in place, so they mutate it.

Modern JavaScript leans heavily on higher-order array methods, which take a function and apply it to each element, returning a new array without touching the original. map transforms every item, filter keeps the items that pass a test, find returns the first match, and reduce combines all items into a single value such as a sum. Chaining them, as in scores.filter(…).map(…), replaces many hand-written loops with short, readable steps.

One trap catches everyone: sort() with no argument sorts items as strings, so [10, 9, 1].sort() gives [1, 10, 9], because "10" comes before "9" alphabetically. For numbers you must pass a comparator function: sort((a, b) => a - b) sorts ascending. sort also mutates the array. Use slice to copy part (or all) of an array without changing it, and the spread syntax [...a, ...b] to combine arrays.

Mutate
Change in place; push, pop, sort, and splice mutate, while map and filter don't.
Higher-order method
An array method that takes a function, such as map, filter, reduce.
Comparator
The function passed to sort, e.g. (a, b) => a - b for numbers.
const fruits = ["apple", "banana", "cherry"];
fruits[0];                      // "apple"
fruits.length;                  // 3
fruits.push("date");           // append — ["apple","banana","cherry","date"]
fruits.pop();                   // remove last — returns "date"
fruits.unshift("avocado");     // prepend
fruits.shift();                 // remove first
fruits.includes("banana");     // true
fruits.indexOf("cherry");      // 2
fruits.reverse();               // reverses in place
fruits.slice(1, 3);            // copy of elements [1] to [2]
fruits.join(" | ");            // "apple | banana | cherry"

Higher-Order Array Methods

const nums = [1, 2, 3, 4, 5];

// map — transform each element, returns new array
nums.map(n => n * 2);          // [2,4,6,8,10]

// filter — keep elements that pass a test
nums.filter(n => n > 2);        // [3,4,5]

// reduce — accumulate to a single value
nums.reduce((sum, n) => sum + n, 0); // 15

// find — first matching element
nums.find(n => n > 3);          // 4

// some / every
nums.some(n => n > 4);          // true (at least one)
nums.every(n => n > 0);         // true (all pass)
Exercise — your turn

First see the string-sort trap, then push 95, sort numerically, slice the top two, and use map, filter, and reduce for the last three lines.

console.log(`Default sort: ${[10, 9, 1].sort().join(", ")}`);

const scores = [88, 92, 75];
// TODO: push 95, then sort numerically with a comparator
console.log(`Scores: ${scores.join(", ")}`);
console.log(`Top two: ${scores.join(", ")}`);   // TODO: slice the last two

const curved = scores;    // TODO: map: add 5 but never above 100 (Math.min)
const passing = scores;   // TODO: filter: 80 and over
const total = 0;          // TODO: reduce to the sum
console.log(`Curved: ${curved.join(", ")}`);
console.log(`80 and over: ${passing.join(", ")}`);
console.log(`Average: ${total / scores.length}`);
Show solution
console.log(`Default sort: ${[10, 9, 1].sort().join(", ")}`);

const scores = [88, 92, 75];
scores.push(95);
scores.sort((a, b) => a - b);
console.log(`Scores: ${scores.join(", ")}`);
console.log(`Top two: ${scores.slice(-2).join(", ")}`);

const curved = scores.map(s => Math.min(s + 5, 100));
const passing = scores.filter(s => s >= 80);
const total = scores.reduce((sum, s) => sum + s, 0);
console.log(`Curved: ${curved.join(", ")}`);
console.log(`80 and over: ${passing.join(", ")}`);
console.log(`Average: ${total / scores.length}`);
Output
Default sort: 1, 10, 9
Scores: 75, 88, 92, 95
Top two: 92, 95
Curved: 80, 93, 97, 100
80 and over: 88, 92, 95
Average: 87.5
Explain it simply

Why does [10, 9, 1].sort() give [1, 10, 9]? Explain it with a dictionary.

Reveal a plain-language answer

Without instructions, sort files everything the way a dictionary does, letter by letter. "1" starts with 1, "10" also starts with 1 (and then 0), and "9" starts with 9, so the dictionary order is 1, 10, 9. It's comparing the spelling of the numbers, not their size. The comparator (a, b) => a - b tells it to compare the actual amounts instead.

6. Objects

An object groups related data under named properties: { name: "Ada", age: 36 }. Each property is a key–value pair, where the key is a string and the value can be anything, including another object, an array, or a function (in which case it's called a method). You read and write properties with dot notation, person.name, or with bracket notation, person["name"]. The bracket form is needed when the key is held in a variable, as in counts[word]. Assigning to a new key adds it, and delete removes one.

Objects are reference values: a variable doesn't hold the object itself but a reference to it. So const b = a doesn't copy an object; both names point at the same one, and a change through either is visible through both. That also explains why a const object's properties can still change: const fixes the reference, not the contents. To make a shallow copy, use the spread syntax: { ...a }.

Modern syntax makes objects pleasant to work with. Destructuring pulls properties out into variables in one line: const { name, city } = person;. Object.keys, Object.values, and Object.entries give you a property list to loop over. JSON (JavaScript Object Notation) is the text format based on this syntax that APIs use to exchange data: JSON.stringify turns an object into JSON text, and JSON.parse turns the text back into an object.

Property
A key–value pair on an object; a property whose value is a function is a method.
Destructuring
Unpacking properties into variables: const { a, b } = obj;
JSON
A text format for data based on object syntax; JSON.stringify / JSON.parse.
const person = {
    name: "Alice",
    age: 30,
    city: "NYC",
};

// Access
person.name;                    // "Alice" (dot notation)
person["age"];                 // 30     (bracket notation)

// Add / update / delete
person.email = "a@b.com";       // add
person.age   = 31;              // update
delete person.city;             // remove key

// Destructuring
const { name, age } = person;   // name = "Alice", age = 31

// Spread operator
const updated = { ...person, age: 32 }; // copy with override

// Keys and values
Object.keys(person);           // ["name","age","email"]
Object.values(person);         // ["Alice",31,"a@b.com"]
Object.entries(person);        // [["name","Alice"],["age",31],…]
Exercise — your turn

Add a city property and bump the age, destructure name and city, list the keys, then count words using bracket notation and ??. Finish with JSON.stringify.

const person = { name: "Ada", age: 36 };
// TODO: add city "London" and add 1 to age

const name = "", city = "";   // TODO: destructure name and city from person
console.log(`${name} lives in ${city}.`);
console.log(`Keys: ${""}`);    // TODO: Object.keys(person) joined with ", "

const counts = {};
for (const word of "the cat and the hat".split(" ")) {
  // TODO: counts[word] = (existing count, or 0) + 1
}
console.log(JSON.stringify(counts));
console.log(JSON.stringify(person));
Show solution
const person = { name: "Ada", age: 36 };
person.city = "London";
person.age += 1;

const { name, city } = person;
console.log(`${name} lives in ${city}.`);
console.log(`Keys: ${Object.keys(person).join(", ")}`);

const counts = {};
for (const word of "the cat and the hat".split(" ")) {
  counts[word] = (counts[word] ?? 0) + 1;
}
console.log(JSON.stringify(counts));
console.log(JSON.stringify(person));
Output
Ada lives in London.
Keys: name, age, city
{"the":2,"cat":1,"and":1,"hat":1}
{"name":"Ada","age":37,"city":"London"}
Explain it simply

If person is declared with const, how can person.age += 1 still work?

Reveal a plain-language answer

const glues the label to the box, not the lid. person will always point at the same object, and you can't swap in a different one, but you're still free to open the box and change what's inside. If you truly want the contents frozen, Object.freeze(person) locks the lid too.

7. Control Structures

Statements normally run top to bottom. Control structures let a program choose which code runs, based on a condition. The if statement evaluates its condition, in parentheses, for truthiness, and runs its block only if it's truthy. else if adds further tests, and else handles everything left. Only the first matching branch runs, so order the tests from most specific to least. Always use braces, even for one-line branches, so that adding a second line later can't silently fall outside the if.

A switch statement compares one value against a list of case labels using strict equality (===) and jumps to the match, with default as the catch-all. Like C, JavaScript's switch falls through into the following case unless each case ends with break (or return). For a compact two-way choice inside an expression, use the ternary operator: n % 2 === 0 ? "even" : "odd".

Choosing a default value is a decision so common that JavaScript has two operators for it, and the difference matters. a || b falls back to b whenever a is falsy, which wrongly discards legitimate values like 0 or an empty string. a ?? b falls back only when a is null or undefined. For settings where 0 is a real answer, such as volume, quantity, or a score, ?? is the correct choice. Optional chaining, user?.address?.city, safely reads a nested property and gives undefined instead of crashing when a link is missing.

Condition
An expression evaluated for truthiness to choose a branch.
Fall-through
In switch, continuing into the next case when there's no break.
|| vs ??
Default when falsy / default only when null or undefined.

if / else if / else

const score = 75;

if (score >= 90) {
    console.log("A");
} else if (score >= 75) {
    console.log("B");
} else if (score >= 60) {
    console.log("C");
} else {
    console.log("F");
}

switch

const day = "Mon";

switch (day) {
    case "Mon":
        console.log("Monday");
        break;
    case "Fri":
        console.log("Friday");
        break;
    default:
        console.log("Other day");
}

Ternary & Nullish Coalescing

// Ternary
const status = age >= 18 ? "adult" : "minor";

// Nullish coalescing — default if null or undefined
const username = inputName ?? "Guest";

// Optional chaining — safe property access
const city = user?.address?.city;   // undefined instead of error
Exercise — your turn

Finish grade (90+ A, 80+ B, 70+ C, 60+ D, else F), write the switch with a break after every case, use a ternary for even/odd, and compare || with ?? when the value is 0.

function grade(score) {
  // TODO: if / else if / else returning "A", "B", "C", "D" or "F"
}

console.log(`${grade(95)} ${grade(82)} ${grade(64)} ${grade(40)}`);

const day = 3;
let dayName;
// TODO: switch (day): 1 Monday, 2 Tuesday, 3 Wednesday, default "some other day"
console.log(`Day ${day} is ${dayName}`);

const n = 7;
console.log(`${n} is ${""}`);   // TODO: "even" or "odd" with ? :

const volume = 0;
console.log(`volume || 10 = ${volume || 10}, volume ?? 10 = ${volume ?? 10}`);
Show solution
function grade(score) {
  if (score >= 90) {
    return "A";
  } else if (score >= 80) {
    return "B";
  } else if (score >= 70) {
    return "C";
  } else if (score >= 60) {
    return "D";
  } else {
    return "F";
  }
}

console.log(`${grade(95)} ${grade(82)} ${grade(64)} ${grade(40)}`);

const day = 3;
let dayName;
switch (day) {
  case 1:
    dayName = "Monday";
    break;
  case 2:
    dayName = "Tuesday";
    break;
  case 3:
    dayName = "Wednesday";
    break;
  default:
    dayName = "some other day";
}
console.log(`Day ${day} is ${dayName}`);

const n = 7;
console.log(`${n} is ${n % 2 === 0 ? "even" : "odd"}`);

const volume = 0;
console.log(`volume || 10 = ${volume || 10}, volume ?? 10 = ${volume ?? 10}`);
Output
A B D F
Day 3 is Wednesday
7 is odd
volume || 10 = 10, volume ?? 10 = 0
Explain it simply

A user sets the volume to 0. Why does volume || 10 turn it back up to 10, while volume ?? 10 leaves it at 0?

Reveal a plain-language answer

|| asks “is this falsy?”, and 0 counts as falsy, so it treats a deliberate 0 as if nothing were set and swaps in 10. ?? asks the narrower question “is this missing?”, meaning null or undefined. 0 isn't missing; it's a real choice, so ?? respects it.

8. Loops

A loop repeats a block of code. Each pass through its body is an iteration. JavaScript's classic for loop has three parts: an initialiser, a condition checked before each iteration, and an update run after each one, as in for (let i = 0; i < 5; i++). Use it when you need a counter. while repeats as long as its condition is true, and do … while checks afterwards, so it always runs at least once.

Two loops iterate over things directly, and they are easy to mix up. for…of visits the values of an iterable, such as an array, a string, a Map, or a Set: for (const fruit of fruits). This is what you want for arrays. for…in visits the keys (property names) of an object: for (const key in stock). Using for…in on an array gives you the indexes as strings, which is rarely what you want. A handy rule: of for values, in for keys.

Inside any loop, break exits immediately and continue skips to the next iteration. Declare loop counters with let, so each iteration gets its own fresh copy, which matters when callbacks created inside the loop use the counter later. Many loops over arrays can be replaced by the higher-order methods from section 5 (forEach, map, filter), which state the intent more directly.

for…of
Loops over the values of an iterable (arrays, strings, Maps, Sets).
for…in
Loops over the property names (keys) of an object.
Iteration
One pass through a loop's body.

for

for (let i = 0; i < 5; i++) {
    console.log(i);
}
// 0 1 2 3 4

for...of (arrays)

const fruits = ["apple", "banana", "cherry"];

for (const fruit of fruits) {
    console.log(fruit);
}

for...in (objects)

const person = { name: "Alice", age: 30 };

for (const key in person) {
    console.log(`${key}: ${person[key]}`);
}

while

let i = 0;
while (i < 5) {
    console.log(i);
    i++;
}
Use break to exit a loop early, and continue to skip to the next iteration.
Exercise — your turn

Write FizzBuzz for 1 to 15 with a classic for loop, then use for…of for the fruit values and for…in for the stock keys.

const parts = [];
// TODO: for i from 1 to 15, push "FizzBuzz", "Fizz", "Buzz" or String(i)
console.log(parts.join(" "));

// TODO: for...of over ["apple", "banana"], printing "I like apple" etc.

const stock = { apple: 3, banana: 5 };
// TODO: for...in over stock, printing "apple: 3" and "banana: 5"
Show solution
const parts = [];
for (let i = 1; i <= 15; i++) {
  if (i % 15 === 0) {
    parts.push("FizzBuzz");
  } else if (i % 3 === 0) {
    parts.push("Fizz");
  } else if (i % 5 === 0) {
    parts.push("Buzz");
  } else {
    parts.push(String(i));
  }
}
console.log(parts.join(" "));

for (const fruit of ["apple", "banana"]) {
  console.log(`I like ${fruit}`);
}

const stock = { apple: 3, banana: 5 };
for (const key in stock) {
  console.log(`${key}: ${stock[key]}`);
}
Output
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
I like apple
I like banana
apple: 3
banana: 5
Explain it simply

What's the difference between for…of and for…in? Explain it with a row of lockers.

Reveal a plain-language answer

for…of walks down the row and hands you what's inside each locker, the values. for…in walks down the row and reads you the number on each door, the keys. For an array you almost always want the contents (of); for an object's named properties you usually want the labels (in), and then you open each one with obj[key].

9. Functions

A function is a reusable block of code that takes parameters and can return a value. JavaScript has several ways to write one. A function declaration, function maxOf(a, b) { … }, is hoisted, meaning it can be called from anywhere in its scope, even above the line where it's written. A function expression assigns a function to a variable. An arrow function, (a, b) => a + b, is a shorter expression form. When its body is a single expression, that expression is returned automatically with no braces or return needed.

Parameters can have default values, (name = "World") => …, which are used when the caller leaves an argument out. A rest parameter, ...nums, collects any number of arguments into a real array. A function without a return gives back undefined. In JavaScript, functions are first-class values: they can be stored in variables, passed as arguments (as with map and addEventListener), and returned from other functions.

That last ability leads to one of JavaScript's most important ideas, the closure. When a function is created inside another function, it keeps access to the outer function's variables, even after the outer function has finished running. In makeCounter, the inner function remembers count, and each call updates the same private variable, which no other code can touch. Closures are how JavaScript creates private state, and they're at work every time a callback uses a variable from the surrounding code.

Arrow function
(params) => expression, a compact function; a single-expression body is returned automatically.
Rest parameter
...args, which collects the remaining arguments into an array.
Closure
A function that remembers the variables of the scope it was created in.

Function Declaration

function greet(name) {
    return `Hello, ${name}!`;
}
greet("Alice");              // "Hello, Alice!"

Arrow Functions

// Single expression — implicit return
const double = n => n * 2;
double(5);                    // 10

// Multiple params
const add = (a, b) => a + b;
add(3, 4);                    // 7

// Block body — explicit return needed
const greet = (name) => {
    const msg = `Hello, ${name}!`;
    return msg;
};

Default Parameters & Rest

// Default parameter
function greet(name = "World") {
    return `Hello, ${name}!`;
}
greet();                      // "Hello, World!"

// Rest parameter — collects remaining args into array
function sum(...nums) {
    return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4);             // 10
Exercise — your turn

Write maxOf as a declaration, greet as an arrow function with a default parameter, and total with a rest parameter and reduce. Then finish makeCounter so the returned function remembers its count between calls.

function maxOf(a, b) {
  // TODO: return the larger value
}

const greet = () => "";   // TODO: default name "World", return `Hello, ${name}!`

const total = () => 0;    // TODO: rest parameter ...nums, reduce to the sum

function makeCounter() {
  let count = 0;
  // TODO: return an arrow function that adds 1 to count and returns it
}

console.log(`maxOf(4, 9) = ${maxOf(4, 9)}`);
console.log(greet());
console.log(greet("Ada"));
console.log(`Sum: ${total(1, 2, 3, 4, 5)}`);

const next = makeCounter();
next();
next();
console.log(`Counter: ${next()}`);
Show solution
function maxOf(a, b) {
  return a > b ? a : b;
}

const greet = (name = "World") => `Hello, ${name}!`;

const total = (...nums) => nums.reduce((sum, n) => sum + n, 0);

function makeCounter() {
  let count = 0;
  return () => {
    count++;
    return count;
  };
}

console.log(`maxOf(4, 9) = ${maxOf(4, 9)}`);
console.log(greet());
console.log(greet("Ada"));
console.log(`Sum: ${total(1, 2, 3, 4, 5)}`);

const next = makeCounter();
next();
next();
console.log(`Counter: ${next()}`);
Output
maxOf(4, 9) = 9
Hello, World!
Hello, Ada!
Sum: 15
Counter: 3
Explain it simply

How does the counter keep counting when makeCounter finished running long ago? Explain a closure with a backpack.

Reveal a plain-language answer

When makeCounter builds the inner function, the inner function packs a backpack containing the variables it can see, including count. makeCounter then goes home, but the inner function carries the backpack wherever it goes. Every call opens the same backpack and updates the same count, and nobody else can reach into it.

10. DOM Manipulation

When a browser loads a page, it reads the HTML and builds the DOM (Document Object Model): a tree of objects in memory, one node for every element, attribute, and piece of text. The <html> element is the root, <body> is its child, a <ul> inside the body is the body's child, and so on. The screen shows whatever the DOM currently contains, so when JavaScript changes the DOM, the page updates immediately. JavaScript reaches the tree through the global document object.

Working with the DOM follows a simple pattern: select, then change. document.querySelector(css) returns the first element matching a CSS selector, such as "#title" for an id or ".card" for a class, and querySelectorAll returns all matches as a list. Once you have an element you can change its text with textContent, toggle CSS classes with classList.add, remove, and toggle, and read or set attributes with getAttribute and setAttribute. Changing classes and letting CSS do the styling is usually better than setting style directly.

To add content, create an element with document.createElement("li"), fill it in, and attach it with appendChild or append. To remove one, call its remove() method. Prefer textContent over innerHTML for putting text on a page. innerHTML parses its value as HTML, so inserting text that came from a user can let them inject their own markup and scripts, a cross-site scripting attack. Finally, a script can only find elements that already exist, so it must run after the HTML it uses: place the <script> at the end of the body, or load it with defer.

DOM
The browser's live tree of objects representing the page; changing it changes the screen.
querySelector
Finds the first element matching a CSS selector; querySelectorAll finds all.
textContent vs innerHTML
Sets plain text safely / parses HTML, so it's risky with user input.

Selecting Elements

// Single element
document.getElementById("myId");
document.querySelector(".my-class");      // first match
document.querySelector("h1");

// Multiple elements (returns NodeList)
document.querySelectorAll(".card");       // all matches

Reading & Changing Content

const el = document.querySelector("#title");

el.textContent;                  // get text
el.textContent = "New Title";   // set text
el.innerHTML   = "<b>Bold</b>"; // set HTML (use carefully)

el.style.color = "red";         // inline style
el.classList.add("active");    // add class
el.classList.remove("active"); // remove class
el.classList.toggle("active"); // toggle class

el.getAttribute("href");        // read attribute
el.setAttribute("href", "/");  // set attribute

Creating & Removing Elements

const p = document.createElement("p");
p.textContent = "New paragraph";
document.body.appendChild(p);   // add to end of body

p.remove();                      // remove from DOM
Exercise — your turn

This one is a web page: save it as practice.html, open it in your browser, and press F12 to see the Console. Change the title, add two list items with createElement, and mark the first item done.

<h1 id="title">My list</h1>
<ul id="todo">
  <li>Buy milk</li>
</ul>

<script>
  const title = document.querySelector("#title");
  // TODO: change the title's text to "Shopping list"

  const list = document.querySelector("#todo");
  for (const item of ["Bread", "Eggs"]) {
    // TODO: create an <li>, set its textContent to item, append it to list
  }

  // TODO: add the class "done" to the first <li> in the list

  const items = list.querySelectorAll("li");
  console.log(`${title.textContent}: ${items.length} items`);
  console.log(`First item class: ${items[0].className}`);
  console.log(`Last item: ${items[items.length - 1].textContent}`);
</script>
Show solution
<h1 id="title">My list</h1>
<ul id="todo">
  <li>Buy milk</li>
</ul>

<script>
  const title = document.querySelector("#title");
  title.textContent = "Shopping list";

  const list = document.querySelector("#todo");
  for (const item of ["Bread", "Eggs"]) {
    const li = document.createElement("li");
    li.textContent = item;
    list.appendChild(li);
  }

  list.querySelector("li").classList.add("done");

  const items = list.querySelectorAll("li");
  console.log(`${title.textContent}: ${items.length} items`);
  console.log(`First item class: ${items[0].className}`);
  console.log(`Last item: ${items[items.length - 1].textContent}`);
</script>
Output
Shopping list: 3 items
First item class: done
Last item: Eggs
Explain it simply

What is the DOM, and why does changing it change what you see? Explain it with a stage play.

Reveal a plain-language answer

The HTML file is the script of the play, written once. The DOM is the actual stage: the real actors and props standing there right now. The audience (the screen) only ever sees the stage. JavaScript is the stage crew, who can move props, swap actors, and add scenery mid-performance. The written script doesn't change, but the stage does, so what the audience sees changes too.

11. Events

Web pages are event-driven. Instead of running from top to bottom and finishing, a page sets up code to run when something happens: a click, a key press, a form submission, the page finishing loading. Each of these is an event. You register a function to respond with element.addEventListener("click", handler). The function is called an event listener (or handler), and the browser calls it every time that event happens on that element, passing an event object that describes what happened.

The event object carries the details. event.target is the exact element the event happened on, event.key says which key was pressed in keyboard events, and event.preventDefault() stops the browser's built-in behaviour. For example, it stops a form submission from reloading the page, so your JavaScript can handle it instead. Common event types include click, input (a field's value changed), submit, keydown, and DOMContentLoaded.

Most events bubble: after firing on the element that was clicked, the event travels up through each of its ancestors, triggering their listeners too. That enables event delegation. Instead of attaching a listener to every item in a list, you attach one listener to the list itself and use event.target to see which item was clicked. Delegation uses less code and memory, and it automatically works for items added to the list later. Code on this site never uses inline onclick="…" attributes; addEventListener in a separate script is the modern, secure approach.

Event listener
A function registered with addEventListener that runs when an event occurs.
event.target
The element the event actually happened on.
Bubbling / delegation
Events travel up to ancestors; one listener on a parent can handle all its children.
const btn = document.querySelector("#myBtn");

// Add event listener
btn.addEventListener("click", (event) => {
    console.log("Button clicked!");
    console.log(event.target);   // the element that was clicked
});

// Common event types
// "click"      — mouse click
// "dblclick"   — double click
// "mouseover"  — hover
// "keydown"    — key pressed
// "keyup"      — key released
// "submit"     — form submitted
// "input"      — input value changed
// "DOMContentLoaded" — page loaded

// Prevent default behaviour (e.g. form submission)
document.querySelector("form").addEventListener("submit", (e) => {
    e.preventDefault();
    console.log("Form intercepted");
});
Exercise — your turn

Another web page: save it as practice.html, open it, and check the Console. Make the button count its clicks, and use one listener on the <ul> (delegation) to report which item was picked. The last lines simulate the clicks for you.

<button id="like">Like</button>
<span id="count">0</span>
<ul id="menu">
  <li>Home</li>
  <li>About</li>
</ul>

<script>
  let likes = 0;
  const btn = document.querySelector("#like");
  const countEl = document.querySelector("#count");

  // TODO: on "click", add one to likes and show it in countEl

  // TODO: ONE "click" listener on #menu that logs `You picked ${event.target.textContent}`

  // Simulate the user: three clicks on the button, one on "About"
  btn.click();
  btn.click();
  btn.click();
  console.log(`Likes: ${countEl.textContent}`);
  document.querySelectorAll("#menu li")[1].click();
</script>
Show solution
<button id="like">Like</button>
<span id="count">0</span>
<ul id="menu">
  <li>Home</li>
  <li>About</li>
</ul>

<script>
  let likes = 0;
  const btn = document.querySelector("#like");
  const countEl = document.querySelector("#count");

  btn.addEventListener("click", () => {
    likes++;
    countEl.textContent = likes;
  });

  document.querySelector("#menu").addEventListener("click", (event) => {
    console.log(`You picked ${event.target.textContent}`);
  });

  // Simulate the user: three clicks on the button, one on "About"
  btn.click();
  btn.click();
  btn.click();
  console.log(`Likes: ${countEl.textContent}`);
  document.querySelectorAll("#menu li")[1].click();
</script>
Output
Likes: 3
You picked About
Explain it simply

Why can one listener on the <ul> handle clicks on every <li>? Explain bubbling with an office building.

Reveal a plain-language answer

When someone rings a doorbell in the building, the message goes to that office first, then up to the floor manager, then to the front desk. Every level hears about it. Bubbling works the same way: a click on an <li> is also reported to its <ul>. So instead of posting a guard at every office door, you put one receptionist at the front desk who checks event.target to see which door was rung. New offices are covered automatically.

12. Async JavaScript

JavaScript is single-threaded: it runs one piece of code at a time. If it simply stopped and waited for slow operations, such as a network request, a timer, or reading a file, the whole page would freeze. Instead, JavaScript is asynchronous. It starts the slow operation, hands it off to the browser or Node.js, and carries on with other work. When the result is ready, a callback is placed in a queue, and the event loop runs it once the current code has finished. This is why a setTimeout of 0 milliseconds still runs after all the ordinary code below it.

A Promise is an object representing a result that isn't available yet. It starts pending, then becomes either fulfilled with a value or rejected with an error. You attach handlers with .then() for success and .catch() for failure. Promise callbacks go into a higher-priority queue (the microtask queue) than timers, so a resolved promise's .then runs before a setTimeout(…, 0) that was scheduled earlier. fetch(), the browser's function for making HTTP requests, returns a Promise.

async/await is the modern way to work with promises, and it lets asynchronous code read like ordinary top-to-bottom code. Inside a function marked async, await somePromise pauses that function until the promise settles and then gives you its value. The rest of the program keeps running in the meantime. A rejected promise makes await throw, so you handle failures with an ordinary try/catch. An async function always returns a Promise itself.

Event loop
The mechanism that runs queued callbacks once the current code has finished.
Promise
An object for a future result: pending, then fulfilled or rejected.
async / await
Syntax that pauses an async function until a promise settles, without blocking the page.

setTimeout & setInterval

// Run once after 2 seconds
setTimeout(() => {
    console.log("2 seconds later");
}, 2000);

// Run every second (returns an ID to cancel it)
const id = setInterval(() => {
    console.log("tick");
}, 1000);
clearInterval(id);              // stop it

Promises

const promise = new Promise((resolve, reject) => {
    const success = true;
    if (success) resolve("Done!");
    else         reject("Failed!");
});

promise
    .then(result => console.log(result))  // "Done!"
    .catch(err   => console.error(err));   // "Failed!"

async / await (preferred)

// fetch() returns a Promise — async/await makes it read like sync code
async function getUser() {
    try {
        const response = await fetch("https://api.example.com/user/1");
        const data     = await response.json();
        console.log(data.name);
    } catch (error) {
        console.error("Error:", error);
    }
}

getUser();
Always wrap await calls in a try/catch block to handle network errors gracefully.
Exercise — your turn

Run with node practice.js. Before finishing it, predict the order of lines A–D. Then complete main: await the wait, await fakeFetch(1), and catch the failure from fakeFetch(2).

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function fakeFetch(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id, name: "Ada" });
      } else {
        reject(new Error(`User ${id} not found`));
      }
    }, 50);
  });
}

async function main() {
  console.log("A: start");
  setTimeout(() => console.log("D: timeout"), 0);
  Promise.resolve().then(() => console.log("C: promise"));
  console.log("B: end of the synchronous code");

  // TODO: await wait(100)
  // TODO: const user = await fakeFetch(1); then log `Loaded ${user.name}`
  // TODO: try { await fakeFetch(2) } catch (err) { log `Error: ${err.message}` }
}

main();
Show solution
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function fakeFetch(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id, name: "Ada" });
      } else {
        reject(new Error(`User ${id} not found`));
      }
    }, 50);
  });
}

async function main() {
  console.log("A: start");
  setTimeout(() => console.log("D: timeout"), 0);
  Promise.resolve().then(() => console.log("C: promise"));
  console.log("B: end of the synchronous code");

  await wait(100);
  const user = await fakeFetch(1);
  console.log(`Loaded ${user.name}`);

  try {
    await fakeFetch(2);
  } catch (err) {
    console.log(`Error: ${err.message}`);
  }
}

main();
Output
A: start
B: end of the synchronous code
C: promise
D: timeout
Loaded Ada
Error: User 2 not found
Explain it simply

Why does a setTimeout with a 0 ms delay still run after the lines below it? Explain the event loop with a restaurant.

Reveal a plain-language answer

There's one waiter (JavaScript). When you order something that takes time, the waiter doesn't stand at the kitchen door; they pass the ticket to the kitchen and keep serving other tables. A 0 ms timer is a dish that's ready instantly, but the waiter still finishes the table they're at before picking it up. Promises are the VIP tickets: they're collected before the regular ones, which is why C prints before D.