A quick-reference and hands-on guide to the fundamentals of JavaScript programming.
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:
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.
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.
document, while Node gives you files and servers.<script> tags or loaded from .js files; (optional, but recommended)// or /* ... */myVar and myvar are different// Inline in HTML
<script>
console.log("Hello, World!");
</script>
// External file
<script src="app.js"></script>
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.
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.
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/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"
| Type | Example | Description |
|---|---|---|
string | "hello" | Text |
number | 42, 3.14 | All numbers (int and float) |
boolean | true / false | Boolean values |
null | null | Intentional empty value |
undefined | undefined | Variable declared but not set |
object | { key: val } | Key-value pairs |
array | [1, 2, 3] | Ordered list (technically an object) |
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
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}`);
Ada is 36 years old. Next year: 37 typeof age: number typeof name: string typeof missing: undefined
Why is const the recommended default, when let can do everything const can?
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.
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 "".
"5" + 3 becoming "53".===.0, "", null, undefined, NaN are falsy.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
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
=== instead of ==. Loose equality causes surprising results: "5" == 5 is true, but "5" === 5 is false.a && b // AND
a || b // OR
!a // NOT
a ?? b // Nullish coalescing — use b if a is null/undefined
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}`);
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}`);
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
Why is "5" + 3 equal to "53" but "5" - 3 equal to 2?
+ 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.
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.
${expression} is evaluated and inserted.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"
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
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)}|`);
Ada Lovelace (12 chars) Initials: A.L. Coffee | 3.50|
Why do people prefer backtick template literals over joining strings with +?
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.
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.
push, pop, sort, and splice mutate, while map and filter don't.map, filter, reduce.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"
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)
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}`);
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}`);
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
Why does [10, 9, 1].sort() give [1, 10, 9]? Explain it with a dictionary.
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.
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.
const { a, b } = obj;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],…]
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));
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));
Ada lives in London.
Keys: name, age, city
{"the":2,"cat":1,"and":1,"hat":1}
{"name":"Ada","age":37,"city":"London"}
If person is declared with const, how can person.age += 1 still work?
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.
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.
switch, continuing into the next case when there's no break.null or undefined.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");
}
const day = "Mon";
switch (day) {
case "Mon":
console.log("Monday");
break;
case "Fri":
console.log("Friday");
break;
default:
console.log("Other day");
}
// 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
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}`);
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}`);
A B D F Day 3 is Wednesday 7 is odd volume || 10 = 10, volume ?? 10 = 0
A user sets the volume to 0. Why does volume || 10 turn it back up to 10, while volume ?? 10 leaves it at 0?
|| 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.
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 (let i = 0; i < 5; i++) {
console.log(i);
}
// 0 1 2 3 4
const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
console.log(fruit);
}
const person = { name: "Alice", age: 30 };
for (const key in person) {
console.log(`${key}: ${person[key]}`);
}
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
break to exit a loop early, and continue to skip to the next iteration.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"
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]}`);
}
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
What's the difference between for…of and for…in? Explain it with a row of lockers.
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].
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.
(params) => expression, a compact function; a single-expression body is returned automatically....args, which collects the remaining arguments into an array.function greet(name) {
return `Hello, ${name}!`;
}
greet("Alice"); // "Hello, Alice!"
// 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 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
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()}`);
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()}`);
maxOf(4, 9) = 9 Hello, World! Hello, Ada! Sum: 15 Counter: 3
How does the counter keep counting when makeCounter finished running long ago? Explain a closure with a backpack.
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.
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.
querySelectorAll finds all.// Single element
document.getElementById("myId");
document.querySelector(".my-class"); // first match
document.querySelector("h1");
// Multiple elements (returns NodeList)
document.querySelectorAll(".card"); // all matches
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
const p = document.createElement("p");
p.textContent = "New paragraph";
document.body.appendChild(p); // add to end of body
p.remove(); // remove from DOM
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>
<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>
Shopping list: 3 items First item class: done Last item: Eggs
What is the DOM, and why does changing it change what you see? Explain it with a stage play.
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.
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.
addEventListener that runs when an event occurs.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");
});
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>
<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>
Likes: 3 You picked About
Why can one listener on the <ul> handle clicks on every <li>? Explain bubbling with an office building.
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.
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.
// 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
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!"
// 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();
await calls in a try/catch block to handle network errors gracefully.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();
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();
A: start B: end of the synchronous code C: promise D: timeout Loaded Ada Error: User 2 not found
Why does a setTimeout with a 0 ms delay still run after the lines below it? Explain the event loop with a restaurant.
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.