Web Development

Complete JavaScript Course

Learn JavaScript from scratch, step by step: beginner, intermediate and advanced lessons on variables, functions, arrays, objects, the DOM, events, async/await, fetch and APIs, classes and modern JavaScript, with practical examples and projects.

  • Beginner
  • 16 modules
  • 76 lessons
  • About 7 h 8 min
  • Free, no sign-up

Module 1 of 16

Beginner: Getting Started with JavaScript

Lesson 1.1 5 min read

What Is JavaScript? A Simple Guide for Complete Beginners

JavaScript is the programming language that makes web pages interactive. In this guide you will learn what JavaScript does, where it runs and what you can build with it.

What does JavaScript do on a web page?

You already know that HTML gives a page its content and CSS makes it look nice. But a page with only HTML and CSS mostly just sits there. JavaScript is what makes it do things.

A programming language is a way to give a computer a list of instructions. JavaScript is a programming language that every web browser understands. With JavaScript, a page can react when you click, type, scroll or move the mouse.

Here are some things JavaScript does on websites you use every day:

  • Opens a menu when you tap the "hamburger" icon on a phone
  • Shows "Password is too short" while you are still typing in a signup form
  • Adds a product to your shopping cart without reloading the page
  • Loads more posts when you scroll to the bottom of a social media feed
  • Shows a live clock, a countdown timer or a photo slider

HTML, CSS and JavaScript: the house analogy

Think of a website as a house.

  • HTML is the structure: the walls, the doors, the windows.
  • CSS is the decoration: the paint, the curtains, the furniture.
  • JavaScript is the electricity: it makes the lights turn on when you press a switch, the doorbell ring and the garage door open.

A house without electricity is still a house, but nothing happens in it. In the same way, JavaScript brings a web page to life.

LanguageJobExample
HTMLContent and structureA button with the text "Like"
CSSLook and layoutThe button is blue with rounded corners
JavaScriptBehaviorClicking the button adds 1 to the like counter

A first look at JavaScript in action

Here is a tiny page with a button. Don't worry about understanding every line yet. Just notice that the HTML and CSS look familiar, and the new part is inside the <script> tag.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Like Button</title>
  <style>
    button { padding: 10px 20px; font-size: 18px; border-radius: 8px; }
  </style>
</head>
<body>
  <button id="like">Like (0)</button>

  <script>
    let likes = 0;
    const button = document.querySelector('#like');

    button.addEventListener('click', () => {
      likes = likes + 1;
      button.textContent = `Like (${likes})`;
    });
  </script>
</body>
</html>

Save this as index.html and open it in your browser. Every time you click the button, the number goes up: "Like (1)", "Like (2)" and so on. The script finds the button, listens for clicks, and updates the text each time. You will learn each of these pieces step by step in this course.

JavaScript can also do simple calculations and show the results in a hidden developer area called the console (you'll learn how to open it in the next lesson):

const price = 25;
const quantity = 3;
console.log(price * quantity);
// Output: 75

The first two lines store two numbers. The last line multiplies them and prints the answer, 75, in the console.

Where does JavaScript run?

JavaScript was created in 1995 to run inside web browsers. Today every modern browser, such as Chrome, Firefox, Safari and Edge, has a built-in JavaScript engine. An engine is the part of the browser that reads your code and runs it. You don't need to install anything to start: if you have a browser, you can write JavaScript.

JavaScript can also run outside the browser. Node.js is a free program that lets you run JavaScript on your own computer or on a server (a computer that sends websites to people over the internet). With Node.js, developers build the "back end" of websites, such as login systems and databases, and also command-line tools. In this course we focus on JavaScript in the browser, but everything you learn about the language itself works in Node.js too.

What can you build with JavaScript?

JavaScript is one of the most popular programming languages in the world. Once you know it, you can build:

  • Interactive websites: forms, menus, image galleries, quizzes
  • Web apps: to-do lists, chat apps, online editors, dashboards
  • Browser games: puzzles, card games, simple 2D games
  • Servers and APIs with Node.js
  • Mobile and desktop apps with tools that use JavaScript under the hood

Tip: You don't need to learn everything at once. Start with small things, like changing text when you click a button. Big apps are just many small pieces put together.

Practice exercise

Copy the Like button example into a new file called index.html and open it in your browser. Click the button a few times. Then change the starting text to Like (10) and change let likes = 0; to let likes = 10;. What number do you see after one click?

Hint: the script adds 1 to whatever number is stored in likes, so the first click should show "Like (11)".

Key takeaways

  • JavaScript is a programming language that makes web pages interactive.
  • HTML is the structure, CSS is the decoration, and JavaScript is the electricity.
  • Every modern browser can run JavaScript, so you don't need to install anything.
  • Node.js lets JavaScript run outside the browser, for example on servers.
  • JavaScript and Java are two different languages.

Frequently asked questions

Is JavaScript the same as Java?

No. Java and JavaScript are two completely different languages. The similar name was a marketing choice in the 1990s, when Java was very popular. A common saying is: "Java is to JavaScript as car is to carpet." Java is often used for Android apps and large business systems, while JavaScript is the language of the web browser.

Is JavaScript hard to learn for beginners?

JavaScript is a good first language. You can see results right away in your browser, and you only need a text editor and a browser to start. Like any new skill, it takes practice, so write a little code every day.

Do I need to learn HTML and CSS before JavaScript?

It helps a lot. On the web, JavaScript usually works with HTML elements and CSS styles, so basic knowledge of both makes JavaScript much easier to understand.

Open this lesson on its own page ↗

Module 2 of 16

Beginner: Variables and Data Types

Lesson 2.1 6 min read

JavaScript Variables: let, const and var Explained

JavaScript variables store values so you can use them later. Learn how to declare variables with let and const, why to avoid var, and how to name them well.

What is a variable in JavaScript?

A variable is a named container for a value. Imagine a cardboard box with a label on it. The label is the variable's name, for example username. Inside the box is the value, for example 'Sara'. Whenever you need the value, you just use the label.

Programs use variables all the time: to remember the user's name, the number of items in a shopping cart, the score in a game, or whether someone is logged in.

const username = 'Sara';
let cartItems = 3;

console.log(username);
// Output: Sara
console.log(cartItems);
// Output: 3

Line by line:

  • const username = 'Sara'; creates a box labelled username and puts the text 'Sara' inside.
  • let cartItems = 3; creates a box labelled cartItems and puts the number 3 inside.
  • console.log(username) looks inside the box and prints what it finds.

Declaring, assigning and reassigning variables

Three words you will hear a lot:

  • Declaring a variable means creating it: let score;
  • Assigning a value means putting something inside it with the = sign: score = 0;
  • Reassigning means replacing the value with a new one: score = 10;

You can declare and assign in one step, which is the most common way: let score = 0;

In JavaScript, = does not mean "is equal to" like in math. It means "put the value on the right into the variable on the left".

let score = 0;
console.log(score);
// Output: 0

score = 10;
console.log(score);
// Output: 10

score = score + 5;
console.log(score);
// Output: 15

let nickname;
console.log(nickname);
// Output: undefined

The line score = score + 5; can look strange at first. JavaScript first works out the right side (the current score, 10, plus 5 equals 15), then stores the result back in score. Notice also that nickname was declared but never given a value, so it holds undefined, which means "no value yet".

Common mistake: Writing let again when you reassign. Writing let score = 10; a second time causes SyntaxError: Identifier 'score' has already been declared. Use let only once, when you create the variable. After that, write just score = 10;.

let vs const: use const by default

JavaScript has two modern keywords for variables:

  • const (short for constant) creates a variable that cannot be reassigned. Once the box is filled, you can't swap the value for a different one.
  • let creates a variable that can be reassigned later.
const birthYear = 1998;
let age = 27;

age = 28;          // fine, age was made with let
console.log(age);
// Output: 28

birthYear = 2000;  // error!
Uncaught TypeError: Assignment to constant variable.

A good rule used by most developers: use const by default, and switch to let only when you know the value will change, such as a counter or a score. This makes your code safer and easier to read. When someone sees const, they know that value stays the same in the rest of the code.

A const must also get a value right away. Writing const total; on its own is a SyntaxError.

Note: const stops you from putting a new value in the box. Later in the course you'll see that the contents of a const list or object can still be changed. That's normal and allowed.

Why you should not use var

Before 2015, JavaScript had only one keyword for variables: var. You will still see it in old tutorials and old code. Modern JavaScript replaced it with let and const because var has confusing behaviors:

  • You can declare the same var twice by accident, and JavaScript won't warn you.
  • A var can be used before the line where it is declared (it gives undefined instead of an error), which hides bugs.
  • A var ignores most curly-brace blocks { }, so it can "leak" out of places where you expected it to stay.
var color = 'red';
var color = 'blue';   // no error, the first value is silently replaced
console.log(color);
// Output: blue

With let or const, the second line would give a clear error and help you find the mistake. So: recognize var when you see it, but write let and const in your own code.

Variable naming rules and camelCase

JavaScript has some strict rules for names:

  • Names can contain letters, digits, _ and $.
  • Names cannot start with a digit. 2ndPlace is not allowed; secondPlace is.
  • Names cannot contain spaces or hyphens. user-name is not allowed.
  • Names are case-sensitive: score and Score are two different variables.
  • You can't use reserved words such as let, const, if, return or class.

On top of the rules, there is a style that almost all JavaScript developers follow: camelCase. You write the first word in lowercase and start every next word with a capital letter, like the humps of a camel: firstName, totalPrice, isLoggedIn.

NameAllowed?Good style?
firstNameYesYes, camelCase
first_nameYesWorks, but not the usual JavaScript style
first nameNoContains a space
1stNameNoStarts with a digit
xYesNo, it doesn't say what it stores
const productName = 'Wireless Headphones';
const productPrice = 59.99;
let isInStock = true;

console.log(productName, productPrice, isInStock);
// Output: Wireless Headphones 59.99 true

These names tell you exactly what each box contains. Good names are like good labels on moving boxes: anyone can find what they need.

Tip: Name true/false variables like a yes/no question: isOpen, hasAccount, canEdit. You'll understand them instantly when you read your code later.

Practice exercise

Create variables for a simple online order: the customer's name, the product name, the price of one item and the quantity. Use const for values that won't change and let for the quantity. Log a sentence about the order. Then change the quantity and log the new total price.

Hint: the total is price * quantity. Because the quantity changes, it must be declared with let.

Key takeaways

  • A variable is a labelled box that stores a value.
  • Use const by default and let only when the value needs to change.
  • Avoid var in new code; it has confusing behaviors.
  • In JavaScript, = means "store this value", not "is equal to".
  • Use clear camelCase names like totalPrice and isLoggedIn.

Frequently asked questions

What is the difference between let and const in JavaScript?

A variable made with let can get a new value later. A variable made with const cannot be reassigned after it is created. Use const when the value stays the same and let for values like counters.

Should I ever use var?

Not in new code. let and const do everything var did, with fewer surprises. You only need to recognize var when reading older code.

Can a JavaScript variable change its type?

Yes. A let variable that holds a number can later hold text. JavaScript allows this, but it usually makes code harder to follow, so try to keep one kind of value per variable.

Open this lesson on its own page ↗

Module 3 of 16

Beginner: Making Decisions with Conditions

Lesson 3.1 5 min read

JavaScript if, else and else if Statements

JavaScript if statements let your code make decisions: do one thing when a condition is true and something else when it is false. Learn if, else, else if and comparison operators.

What is an if statement?

Every day you make decisions based on conditions: if it is raining, take an umbrella; otherwise, wear sunglasses. Programs need to make decisions too. An if statement runs a block of code only when a condition is true.

A condition is a question with a yes/no answer, like "Is the user older than 18?" In JavaScript, the answer is a boolean: true or false.

Comparison operators in JavaScript

To build conditions, you compare values with comparison operators. Each one gives back true or false.

OperatorMeaningExampleResult
===Equal to (same value and type)5 === 5true
!==Not equal to5 !== 3true
>Greater than10 > 7true
<Less than10 < 7false
>=Greater than or equal to18 >= 18true
<=Less than or equal to4 <= 3false

As you saw in the Type Conversion lesson, always use === and !== instead of == and !=.

if and else: a simple age check

Here is the basic shape of an if statement:

const age = 16;

if (age >= 18) {
  console.log('Welcome! You can create an account.');
} else {
  console.log('Sorry, you must be 18 or older to sign up.');
}
// Output: Sorry, you must be 18 or older to sign up.

How it works:

  • After if, the condition goes inside round brackets ( ).
  • The code to run when the condition is true goes inside curly braces { }. This group of code is called a block.
  • else is optional. Its block runs only when the condition is false.

Change age to 21 and run it again. Now you will see the welcome message. Exactly one of the two blocks always runs, never both.

Common mistake: Using a single = inside a condition. if (age = 18) does not compare; it assigns 18 to age. To compare, use ===.

else if: checking several conditions (grade calculator)

When there are more than two possibilities, add else if. JavaScript checks each condition from top to bottom and runs the first block whose condition is true. Then it skips the rest.

const score = 83;
let grade;

if (score >= 90) {
  grade = 'A';
} else if (score >= 80) {
  grade = 'B';
} else if (score >= 70) {
  grade = 'C';
} else if (score >= 60) {
  grade = 'D';
} else {
  grade = 'F';
}

console.log(`Your score is ${score}, so your grade is ${grade}.`);
// Output: Your score is 83, so your grade is B.

A score of 83 is not 90 or more, so the first block is skipped. It is 80 or more, so grade becomes 'B', and all the other checks are skipped. Notice that we didn't need to write score >= 80 && score < 90. The order of the checks does that work for us. If you put score >= 60 first, every passing student would get a D, so order matters.

Also notice let grade; is declared before the if statement. A variable declared inside a block { } with let or const only exists inside that block.

Greeting by time of day with new Date().getHours()

Let's make a page greet the visitor based on the time. new Date() creates a date object for the current moment, and .getHours() gives the current hour from 0 (midnight) to 23 (11 pm), using the visitor's own clock.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Greeting</title>
</head>
<body>
  <h1 id="greeting">Hello!</h1>

  <script>
    const hour = new Date().getHours();
    let message;

    if (hour < 12) {
      message = 'Good morning!';
    } else if (hour < 18) {
      message = 'Good afternoon!';
    } else {
      message = 'Good evening!';
    }

    document.querySelector('#greeting').textContent = message;
    console.log(`It is ${hour}:00, so the message is "${message}"`);
  </script>
</body>
</html>

Open the page and the heading changes to the right greeting. At 9 am you see "Good morning!", at 3 pm "Good afternoon!" and at 8 pm "Good evening!". To test the other messages without waiting, temporarily replace the first line with const hour = 20;.

Nested if statements and block braces

You can put an if statement inside another one. This is called nesting. It's useful when a second question only matters after the first one is answered.

const isLoggedIn = true;
const cartTotal = 45;

if (isLoggedIn) {
  if (cartTotal >= 50) {
    console.log('You get free shipping!');
  } else {
    console.log(`Add $${50 - cartTotal} more for free shipping.`);
  }
} else {
  console.log('Please log in to check out.');
}
// Output: Add $5 more for free shipping.

Notice if (isLoggedIn) without === true. Because isLoggedIn is already a boolean (and truthy values work too), you can use it directly.

Two good habits:

  • Always use curly braces, even for one line. JavaScript lets you skip them for a single statement, but adding a second line later then causes bugs that are hard to spot.
  • Indent the code inside each block (usually 2 spaces) so you can see which code belongs where.

Tip: Try not to nest more than two or three levels deep. Deep nesting is hard to read. In the next lesson you'll learn logical operators, which let you combine conditions into one.

Practice exercise

Write a "movie ticket price" program. Create a variable age. Children under 12 pay $6, people from 12 to 64 pay $12, and people 65 and over pay $8. Log a message like Your ticket costs $12. Test it with the ages 8, 30 and 70.

Hint: use if, two else if checks... or just one else if and an else. Start with the smallest age group: if (age < 12).

Key takeaways

  • An if statement runs a block of code only when its condition is true.
  • else runs when the condition is false; else if adds more conditions, checked from top to bottom.
  • Only the first matching block runs, so the order of your checks matters.
  • Use ===, !==, >, <, >= and <= to build conditions, and never a single =.
  • Always use curly braces and indent your code.

Open this lesson on its own page ↗

Module 4 of 16

Beginner: Loops

Lesson 4.1 6 min read

JavaScript for Loops Explained

A JavaScript for loop repeats a block of code a set number of times. Learn the three parts of a for loop, how to count up and down, and how to loop through strings.

What is a loop in JavaScript?

A loop is a way to run the same code again and again without writing it again and again. Think of a teacher who says "do 10 jumping jacks". She does not say "jump" ten times. She gives one instruction and a number.

Without a loop, printing the numbers 1 to 5 looks like this:

console.log(1);
console.log(2);
console.log(3);
console.log(4);
console.log(5);

This works, but imagine doing it for 1,000 numbers. A loop does the same job in three lines, and it is just as easy for 5 numbers as for 5,000.

The three parts of a for loop

A for loop has three parts inside the round brackets, separated by semicolons:

  1. Start (initialization): create a counter variable, for example let i = 1. This runs once, before the loop begins.
  2. Condition: a test such as i <= 5. Before each round, JavaScript checks it. If it is true, the code runs. If it is false, the loop stops.
  3. Step (update): change the counter after each round, for example i++, which means "add 1 to i".
for (let i = 1; i <= 5; i++) {
  console.log(`Lap number ${i}`);
}
// Output:
// Lap number 1
// Lap number 2
// Lap number 3
// Lap number 4
// Lap number 5

Here is what happens. let i = 1 creates the counter. JavaScript checks 1 <= 5, which is true, so it prints "Lap number 1". Then i++ makes i equal to 2, and the check runs again. When i becomes 6, the condition 6 <= 5 is false and the loop ends. Each single run of the loop body is called an iteration.

Note: Programmers often name the counter i (short for "index"). You can use any name, like lap or count, if it makes your code clearer.

How to count down with a for loop

To count backwards, start high, check that the counter is still big enough, and subtract with i-- (subtract 1).

for (let i = 5; i >= 1; i--) {
  console.log(i);
}
console.log("Lift off!");
// Output: 5, 4, 3, 2, 1 (each on its own line), then "Lift off!"

You can also step by more than 1. i += 2 adds 2 each time, so for (let i = 0; i <= 10; i += 2) gives you the even numbers 0, 2, 4, 6, 8, 10.

How to loop through a string or an array

A string has a length property and each character has a position number called an index. Indexes start at 0, not 1. So the first letter of "Hello" is at index 0 and the last is at index 4.

const word = "Hello";

for (let i = 0; i < word.length; i++) {
  console.log(`Letter ${i}: ${word[i]}`);
}
// Output:
// Letter 0: H
// Letter 1: e
// Letter 2: l
// Letter 3: l
// Letter 4: o

word.length is 5, so the condition i < word.length stops the loop after index 4. word[i] reads the character at position i.

An array is a list of values inside square brackets. You will learn arrays properly in the Arrays module, but they work the same way as strings here: they have a length and indexes that start at 0.

const groceries = ["milk", "bread", "eggs", "apples"];

for (let i = 0; i < groceries.length; i++) {
  console.log(`${i + 1}. ${groceries[i]}`);
}
// Output:
// 1. milk
// 2. bread
// 3. eggs
// 4. apples

We print i + 1 so the list starts at 1 for humans, while groceries[i] still uses the real index starting at 0.

Common mistake: Writing i <= word.length instead of i < word.length. The last index is always length - 1, so <= goes one step too far and gives you undefined.

Example: build a multiplication table

Loops are great for anything with a pattern. Here is the 7 times table:

const number = 7;

for (let i = 1; i <= 10; i++) {
  console.log(`${number} x ${i} = ${number * i}`);
}
// Output:
// 7 x 1 = 7
// 7 x 2 = 14
// ...
// 7 x 10 = 70

Change number to any value and you get a new table. This is the power of loops: one small change, a whole new result.

How to avoid infinite loops

An infinite loop is a loop that never stops, because its condition never becomes false. It can freeze your browser tab.

// Do NOT run this: i never changes, so i <= 5 is always true
// for (let i = 1; i <= 5; ) {
//   console.log(i);
// }

To stay safe, always check three things: the counter starts at the right value, the condition can become false, and the step moves the counter toward that point. Counting up with i-- or counting down with i++ are classic causes of infinite loops.

Tip: If your tab freezes, close it and open a new one. Then look at the step part of your loop first.

You can read more on MDN's for statement page.

Practice exercise

Write a for loop that prints every number from 1 to 20, but next to each one write "even" or "odd", for example 4 is even. Then print the total of all the numbers from 1 to 20 after the loop.

Hint: Use the remainder operator %. If i % 2 === 0, the number is even. For the total, create let total = 0; before the loop and add i to it inside the loop.

Key takeaways

  • A loop runs the same code many times, so you do not repeat yourself.
  • A for loop has three parts: start, condition and step.
  • Strings and arrays have indexes that start at 0, so loop while i < length.
  • Use i-- to count down and i += 2 (or any number) to take bigger steps.
  • Make sure the condition can become false, or you will create an infinite loop.

Frequently asked questions

Why does the for loop start at 0 so often?

Because strings and arrays start counting their positions at 0. Starting your counter at 0 means i matches the index of each item directly.

Can I use const in a for loop counter?

Not for a normal counting loop. The counter changes with i++, and a const variable cannot change, so you will get an error. Use let.

What is the difference between i++ and i += 1?

In a for loop step, there is no difference. Both add 1 to i. i += 1 is useful when you want to change the step to another number later.

Open this lesson on its own page ↗

Module 5 of 16

Beginner: Functions

Lesson 5.1 5 min read

JavaScript Functions: How to Declare and Call Them

A JavaScript function is a named block of code you can run whenever you need it. Learn how to declare a function, call it, and name it well.

Why do functions exist? The recipe analogy

A function is like a recipe. You write the recipe for pancakes once. After that, whenever you want pancakes, you just say "make pancakes" and follow the same steps. You do not rewrite the recipe every morning.

In code, a function is a set of instructions with a name. You write it once and use it as many times as you like. This brings three big benefits:

  • Less repetition: you write the steps one time.
  • Easier changes: fix a bug in one place and it is fixed everywhere.
  • Clearer code: a good name like showWelcomeMessage() tells the reader what happens without reading every line.

How to declare a function in JavaScript

Creating a function is called declaring (or defining) it. A function declaration looks like this:

function greetUser() {
  console.log("Hello! Welcome to CodeCraft.");
  console.log("Let's write some JavaScript today.");
}

Let's look at each part:

  • function is the keyword that tells JavaScript "a function starts here".
  • greetUser is the name you choose.
  • () round brackets come after the name. Later you will put inputs here.
  • { } curly braces hold the body, the code that runs when the function is used.

If you run this code, nothing appears in the console. That is normal. Declaring a function is like writing down a recipe: no pancakes are made yet.

How to call a function

To run the code inside a function, you call it (also called "invoking" it). Write its name followed by round brackets:

function greetUser() {
  console.log("Hello! Welcome to CodeCraft.");
}

greetUser();
greetUser();
// Output:
// Hello! Welcome to CodeCraft.
// Hello! Welcome to CodeCraft.

Each greetUser(); runs the whole body once. We called it twice, so the message appears twice.

Common mistake: Writing greetUser without the brackets. That does not run the function. It only refers to it, like pointing at a recipe book without cooking. Always add () to call a function.

The DRY idea: Don't Repeat Yourself

Programmers have a rule called DRY: Don't Repeat Yourself. If you copy and paste the same lines in several places, those lines probably belong in a function.

Here is a small shop example. Without a function, we repeat the same divider lines:

console.log("==========");
console.log("Order #1 received");
console.log("==========");
console.log("Order #2 received");
console.log("==========");

With a function, the divider lives in one place:

function printDivider() {
  console.log("==========");
}

printDivider();
console.log("Order #1 received");
printDivider();
console.log("Order #2 received");
printDivider();
// Output is the same as before

Now if your boss wants the divider to be ---------- instead, you change one line, not five. In the next lesson you will see how to pass information into functions, so even the order number can be handled by a function.

How to name functions: use verbs

Functions do things, so their names usually start with a verb (an action word). Use camelCase, as you do for variables.

Good nameWhy it works
showMenu()Clear action and target
calculateTotal()Says exactly what it computes
isLoggedIn()Starts with "is", so it answers yes or no
data() (bad)No verb, so you cannot guess what it does

Tip: If you cannot think of a short name for a function, it may be doing too many things. Try splitting it into two smaller functions.

What is hoisting? (in simple words)

JavaScript reads your whole file before it runs it. During that first read, it notes all function declarations. This means you can call a function declaration before the line where you wrote it. This behaviour is called hoisting, as if the function is "lifted" to the top.

sayGoodMorning();

function sayGoodMorning() {
  console.log("Good morning!");
}
// Output: Good morning!

This works only for function declarations (the function name() {} style). In the Arrow Functions lesson you will meet other ways to create functions that are not hoisted like this. Many developers still prefer to declare functions first and call them after, because the code then reads from top to bottom.

Practice exercise

Write a function called printBusinessCard that prints three lines to the console: your name, a job title, and an email address. Call it three times. Then change the job title inside the function and run the code again to see every card update.

Hint: Put three console.log() lines inside the curly braces, then write printBusinessCard(); three times below the function.

Key takeaways

  • A function is a named, reusable block of code, like a recipe.
  • Declare it with function name() { ... } and call it with name();.
  • Follow DRY: when you repeat code, move it into a function.
  • Start function names with a verb, such as showMenu or calculateTotal.
  • Function declarations are hoisted, so they can be called before the line where they are written.

Frequently asked questions

What is the difference between declaring and calling a function?

Declaring creates the function and gives it a name, but runs nothing. Calling, with name(), actually runs the code inside it.

How many times can I call a function?

As many times as you want. That is the whole point of a function: write once, use many times.

Can a function call another function?

Yes. A function body can contain any code, including calls to other functions. This is how big programs are built from small pieces.

Open this lesson on its own page ↗

Module 6 of 16

Intermediate: Arrays

Lesson 6.1 5 min read

JavaScript Arrays: Create, Read and Update Lists

A JavaScript array is an ordered list of values stored in one variable. In this lesson you will learn how to create arrays, read items by index, and add or remove items.

What is an array in JavaScript?

So far, each variable you made held one value: one name, one price, one score. But real programs work with lists all the time: a shopping cart, a list of messages, the songs in a playlist.

An array is a single value that holds many values in order. Think of it like a row of numbered boxes. Each box holds one item, and each box has a number so you can find it again.

You create an array with square brackets []. You separate the items with commas.

const fruits = ["apple", "banana", "cherry"];
const prices = [4.99, 12.5, 0.99];
const emptyCart = [];

console.log(fruits);
// Output: ["apple", "banana", "cherry"]
console.log(emptyCart);
// Output: []

The first line makes an array with three strings. The second holds three numbers. The third is an empty array, which is a normal starting point when you plan to add items later. Each item in an array is called an element.

An array can hold mixed types too: strings, numbers, booleans, even other arrays. In practice, most arrays hold one kind of thing, because that makes them easier to work with.

const mixed = ["Mia", 28, true, ["red", "blue"]];
console.log(mixed.length);
// Output: 4

How to read array items with an index

Every element has a position number called an index. JavaScript counts from 0, not 1. This is called zero-based indexing. So the first item is at index 0, the second at index 1, and so on.

You read an element by putting its index in square brackets after the array name. The length property tells you how many elements the array has.

const colors = ["red", "green", "blue", "yellow"];

console.log(colors[0]);            // Output: red
console.log(colors[2]);            // Output: blue
console.log(colors.length);        // Output: 4
console.log(colors[colors.length - 1]); // Output: yellow
console.log(colors[10]);           // Output: undefined
console.log(colors.at(-1));        // Output: yellow
console.log(colors.at(-2));        // Output: blue

Here is what each line does:

  • colors[0] gets the first element.
  • colors.length is 4, but the last index is 3, because counting starts at 0.
  • colors[colors.length - 1] is the classic way to get the last element.
  • Asking for an index that does not exist gives undefined. There is no error.
  • colors.at(-1) is the modern, shorter way to get the last element. Negative numbers count from the end.

Common mistake: Writing colors[colors.length] to get the last item. That index is one past the end, so you get undefined. Use colors.length - 1 or colors.at(-1).

You can also change an element by assigning to its index: colors[1] = "purple"; replaces "green" with "purple".

How to add and remove items: push, pop, shift and unshift

Arrays come with built-in methods, which are functions that belong to the array. Four of them add or remove items at the start or end.

MethodWhat it doesReturns
push(item)Adds to the endThe new length
pop()Removes from the endThe removed item
unshift(item)Adds to the startThe new length
shift()Removes from the startThe removed item

Let's use them on a to-do list:

const todos = ["Buy milk", "Call Sam"];

todos.push("Pay rent");
console.log(todos);
// Output: ["Buy milk", "Call Sam", "Pay rent"]

todos.unshift("Wake up");
console.log(todos);
// Output: ["Wake up", "Buy milk", "Call Sam", "Pay rent"]

const done = todos.pop();
console.log(done);   // Output: Pay rent

const first = todos.shift();
console.log(first);  // Output: Wake up
console.log(todos);  // Output: ["Buy milk", "Call Sam"]

push and unshift add items. pop and shift take one item out and give it back to you, so you can store it in a variable like done.

Tip: A simple way to remember: push and pop work at the end (like a stack of plates). shift and unshift work at the start.

How to check if an array contains a value

Often you need to ask "is this item in my list?" Use includes() for a yes/no answer, and indexOf() when you need the position.

const tags = ["html", "css", "javascript"];

console.log(tags.includes("css"));       // Output: true
console.log(tags.includes("python"));    // Output: false
console.log(tags.indexOf("javascript")); // Output: 2
console.log(tags.indexOf("python"));     // Output: -1

console.log(Array.isArray(tags));        // Output: true
console.log(Array.isArray("css"));       // Output: false
console.log(typeof tags);                // Output: object

indexOf returns -1 when the item is not found. Array.isArray() is the reliable way to check if something is an array, because typeof just says "object" for arrays.

Why can a const array still change?

You may be surprised that we used const and still changed the array with push. const means the variable cannot be reassigned to a new value. It does not freeze what is inside.

const cart = ["shoes"];
cart.push("socks");    // OK: we change the contents
console.log(cart);     // Output: ["shoes", "socks"]

cart = ["hat"];        // TypeError: Assignment to constant variable.

Think of const like a label glued to one box. You cannot move the label to a different box, but you can still put things in and take things out. Using const for arrays is the normal, recommended style.

Note: You can read more about every array method on MDN's Array reference.

Practice exercise

Create a const array called playlist with three song names. Then:

  1. Add a new song to the end and one to the start.
  2. Log the first and last songs (use at(-1) for the last one).
  3. Remove the first song and log which song was removed.
  4. Log whether your playlist includes a song of your choice.

Hint: You need push, unshift, shift and includes. Log playlist.length after each step to see how it changes.

Key takeaways

  • An array is an ordered list created with [], and each value is an element.
  • Indexes start at 0; the last index is length - 1, and at(-1) gets the last item.
  • push/pop work at the end, unshift/shift work at the start.
  • Use includes for yes/no checks, indexOf for positions, and Array.isArray to test for arrays.
  • A const array cannot be reassigned, but its contents can change.

Frequently asked questions

Why do JavaScript arrays start at 0?

The index means "how many steps from the start". The first item is zero steps away, so it is index 0. Most programming languages work this way.

Can a JavaScript array hold different data types?

Yes. One array can hold strings, numbers, booleans, objects and other arrays. It is usually clearer to keep one type per array.

What happens if I read an index that does not exist?

You get undefined. JavaScript does not throw an error, so check length or use includes when you are not sure.

Open this lesson on its own page ↗

Module 7 of 16

Intermediate: Objects and JSON

Lesson 7.1 5 min read

JavaScript Objects Explained: Properties and Methods

A JavaScript object groups related data and actions under one name using key-value pairs. Learn how to create objects, read and change properties, and add methods.

What is an object in JavaScript?

An array is great for a list of similar things. But how do you describe one thing that has many details, like a user with a name, an email and an age? That is what an object is for.

An object is a collection of properties. Each property has a key (its name) and a value. Think of a contact card in your phone: "Name: Ana", "Phone: 555-1234". The labels are keys, and the details are values.

You create an object with curly braces {}. This is called an object literal.

const user = {
  name: "Ana Silva",
  email: "ana@example.com",
  age: 31,
  isAdmin: false,
};

console.log(user);
// Output: { name: "Ana Silva", email: "ana@example.com", age: 31, isAdmin: false }
console.log(user.name);
// Output: Ana Silva

Each property is written as key: value, separated by commas. Values can be any type: strings, numbers, booleans, arrays, or even other objects.

Dot notation vs bracket notation

There are two ways to read a property.

  • Dot notation: user.email. Short and clear. Use it most of the time.
  • Bracket notation: user["email"]. The key is a string inside brackets. You need it when the key is stored in a variable, or when the key has spaces or dashes.
const product = {
  title: "Desk Lamp",
  price: 29,
  "color option": "black",
};

console.log(product.title);           // Output: Desk Lamp
console.log(product["price"]);        // Output: 29
console.log(product["color option"]); // Output: black

const field = "title";
console.log(product[field]);          // Output: Desk Lamp
console.log(product.field);           // Output: undefined
console.log(product.weight);          // Output: undefined

product[field] uses the value of the variable field, which is "title". But product.field looks for a property literally named "field", which does not exist. Reading a missing property gives undefined, not an error.

Common mistake: Using dot notation with a variable, like product.field, when you mean product[field]. Dot notation always uses the exact word you type.

How to add, update and delete properties

Objects can change after you create them, even if you used const (just like arrays). Assign to a new key to add it, assign to an existing key to update it, and use the delete operator to remove it.

const book = { title: "Dune", author: "Frank Herbert" };

book.year = 1965;          // add
book.title = "Dune (Book 1)"; // update
delete book.author;        // remove

console.log(book);
// Output: { title: "Dune (Book 1)", year: 1965 }
console.log("author" in book);
// Output: false

The in operator checks whether an object has a key. It returns true or false.

What are object methods and this?

A property whose value is a function is called a method. Methods are the actions an object can do. You have already used methods: push is a method of arrays.

Inside a method, the keyword this refers to the object the method was called on. It lets the method read the object's own properties.

const account = {
  owner: "Leo",
  balance: 100,
  deposit(amount) {
    this.balance += amount;
    return this.balance;
  },
  describe() {
    return `${this.owner} has $${this.balance}`;
  },
};

account.deposit(50);
console.log(account.describe());
// Output: Leo has $150

deposit(amount) { ... } is the short method syntax. When we call account.deposit(50), this is account, so this.balance grows from 100 to 150.

Note: Do not use arrow functions as object methods when you need this. Arrow functions do not get their own this, so this.balance would not point to the object. Use the short method syntax shown above.

Nested objects, arrays of objects and shorthand properties

Objects can hold other objects and arrays. You read deeper values by chaining dots. An array of objects is one of the most common data shapes in real apps, for example a list of orders from a server.

const order = {
  id: 1042,
  customer: {
    name: "Priya",
    address: { city: "Lisbon", zip: "1100-001" },
  },
  items: [
    { name: "Mug", price: 9 },
    { name: "Coffee beans", price: 14 },
  ],
};

console.log(order.customer.address.city); // Output: Lisbon
console.log(order.items[1].name);         // Output: Coffee beans
console.log(order.items.length);          // Output: 2

Read order.items[1].name left to right: the order, then its items array, then the second item, then its name.

When a variable has the same name as the key you want, you can use shorthand properties. Instead of { name: name }, just write { name }.

const name = "Tom";
const city = "Oslo";

const person = { name, city, country: "Norway" };
console.log(person);
// Output: { name: "Tom", city: "Oslo", country: "Norway" }

Tip: Shorthand properties are very common when building an object from form values or function parameters. They keep code short and avoid repeating yourself.

Practice exercise

Create an object called pet with a name, a species, an age and an array of favoriteToys. Then:

  1. Log the pet's name with dot notation and its species with bracket notation.
  2. Add a new property vaccinated: true and update the age by 1.
  3. Add a method introduce() that returns a sentence like "Hi, I'm Rex, a 4-year-old dog."
  4. Log the second favorite toy.

Hint: Use this.name and this.age inside introduce, and pet.favoriteToys[1] for the toy.

Key takeaways

  • An object stores related data as key-value pairs inside {}.
  • Use dot notation normally and bracket notation for keys in variables or keys with spaces.
  • You can add, update and delete properties, even on a const object.
  • Methods are functions inside objects; this refers to the object the method was called on.
  • Objects can be nested, arrays of objects are everywhere, and { name } is shorthand for { name: name }.

Frequently asked questions

What is the difference between an array and an object?

An array is an ordered list where you find items by number (index). An object stores named details where you find values by key. Use arrays for lists and objects for describing one thing.

Why do I get undefined when reading an object property?

The property does not exist, or the key is spelled differently. Keys are case-sensitive, so user.Name and user.name are different.

Is an array an object in JavaScript?

Technically yes: typeof [] is "object". Arrays are a special kind of object with numbered keys and a length. Use Array.isArray() to tell them apart.

Open this lesson on its own page ↗

Module 8 of 16

Intermediate: The DOM (Document Object Model)

Lesson 8.1 5 min read

What Is the DOM? Selecting Elements with JavaScript

The DOM is the browser's live model of your web page. In this lesson you will learn what the DOM is and how to select elements with JavaScript using querySelector and friends.

What is the DOM in JavaScript?

When the browser loads an HTML file, it does not keep the text as it is. It reads the HTML and builds a tree of objects in memory. This tree is called the DOM, short for Document Object Model. Every tag becomes an object called a node (or an element when it is a tag). JavaScript can read and change these objects, and the page on the screen updates right away.

Think of a family tree. At the top is one ancestor, and below are children, grandchildren and so on. The DOM works the same way. Take this small page:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>My Shop</title>
  </head>
  <body>
    <h1>Welcome</h1>
    <ul>
      <li>Apples</li>
      <li>Bread</li>
    </ul>
  </body>
</html>

The browser turns it into this tree:

document
└── html
    ├── head
    │   └── title
    │       └── "My Shop"
    └── body
        ├── h1
        │   └── "Welcome"
        └── ul
            ├── li
            │   └── "Apples"
            └── li
                └── "Bread"

The <body> is the parent of <h1> and <ul>. The two <li> elements are children of <ul>, and they are siblings of each other. The text inside a tag is its own small node, called a text node.

At the very top is the document object. It represents the whole page, and it is your starting point for everything in the DOM. Open the browser console on any page and type document.title to see the page title.

How to select elements with querySelector

Before you can change an element, you need to find it. The most useful method is document.querySelector(). You give it a CSS selector (the same selectors you use in CSS), and it returns the first matching element. If nothing matches, it returns null.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Profile</title>
  </head>
  <body>
    <h1 id="username">Maya Lopez</h1>
    <p class="bio">Web developer from Lisbon.</p>
    <p class="bio">Loves coffee and cats.</p>

    <script>
      const heading = document.querySelector("#username");
      const firstBio = document.querySelector(".bio");
      const missing = document.querySelector(".avatar");

      console.log(heading.textContent);
      console.log(firstBio.textContent);
      console.log(missing);
    </script>
  </body>
</html>
Maya Lopez
Web developer from Lisbon.
null

What happens here:

  • "#username" selects by id, just like in CSS.
  • ".bio" selects by class, but querySelector only returns the first .bio paragraph.
  • ".avatar" matches nothing, so you get null. Trying to use missing.textContent would cause an error.

There is also document.getElementById("username"). You pass the id without the #. It is fast and you will see it in a lot of code, but querySelector can do everything it does and more, so many developers use querySelector for consistency.

Common mistake: Writing document.getElementById("#username"). With getElementById you must leave out the #, otherwise it returns null.

How to select many elements with querySelectorAll

To get all matching elements, use document.querySelectorAll(). It returns a NodeList, which is a list of elements. A NodeList looks like an array, but it is not quite one. You can use .length, for...of and .forEach(), but not methods like .map() or .filter(). If you need those, turn it into a real array with Array.from().

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Menu</title>
  </head>
  <body>
    <ul class="menu">
      <li class="dish">Pizza - $12</li>
      <li class="dish">Pasta - $10</li>
      <li class="dish">Salad - $8</li>
    </ul>

    <script>
      const dishes = document.querySelectorAll(".menu .dish");
      console.log(dishes.length);

      for (const dish of dishes) {
        console.log(dish.textContent);
      }

      const names = Array.from(dishes).map((dish) => dish.textContent.split(" - ")[0]);
      console.log(names);
    </script>
  </body>
</html>
3
Pizza - $12
Pasta - $10
Salad - $8
["Pizza", "Pasta", "Salad"]

The selector ".menu .dish" finds every .dish inside .menu. The for...of loop visits each element one by one. Array.from(dishes) makes a real array, so you can use .map() to keep only the dish names.

You can also search inside one element instead of the whole page. For example, menu.querySelectorAll("li") only looks inside menu.

Note: If nothing matches, querySelectorAll returns an empty NodeList (length 0), not null. Your loop simply runs zero times.

Why your script needs defer

JavaScript can only select elements that already exist. The browser reads HTML from top to bottom. If your <script> is in the <head>, it runs before the <body> is built, so querySelector returns null.

You have two good fixes. One is to put the <script> at the end of the <body>, like the examples above. The modern way is to keep the script in the <head> and add the defer attribute:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Deferred script</title>
    <script src="app.js" defer></script>
  </head>
  <body>
    <h1>Hello</h1>
  </body>
</html>
// app.js
const title = document.querySelector("h1");
console.log(title.textContent);
// Output: Hello

defer tells the browser: "Download this file now, but run it only after the whole HTML is read." The page loads fast and your code always finds the elements. defer only works with external files (a src attribute). Scripts with type="module" are deferred automatically.

Practice exercise

Create a page with a heading and a list of five favourite movies. Each <li> should have the class movie. In an external app.js file loaded with defer, select the heading and log its text. Then select all movies, log how many there are, and log each title with its position, like 1. Inception.

Hint: forEach gives you the index as the second argument: movies.forEach((movie, index) => { ... }). Remember that the index starts at 0.

Key takeaways

  • The DOM is a tree of objects that the browser builds from your HTML, and document is its root.
  • querySelector returns the first match or null; querySelectorAll returns a NodeList of all matches.
  • A NodeList works with for...of and forEach; use Array.from() to get array methods like map.
  • Use defer on scripts in the <head> so your code runs after the HTML is ready.

Frequently asked questions

Is the DOM the same as my HTML file?

No. The HTML file is just text. The DOM is the live object tree the browser builds from it. When JavaScript changes the DOM, the page changes, but your HTML file on disk stays the same.

Should I use getElementById or querySelector?

Both work. getElementById is a little faster, but the difference is too small to notice. querySelector accepts any CSS selector, so it is the more flexible choice for everyday code.

Why does querySelector return null?

Usually because the selector has a typo, or because the script ran before the element existed. Check the spelling and load your script with defer.

Open this lesson on its own page ↗

Module 9 of 16

Intermediate: Events and Forms

Lesson 9.1 5 min read

JavaScript Events: addEventListener Explained

Events let your page react when people click, type or move the mouse. This lesson explains JavaScript events and how to use addEventListener to run code when they happen.

What are events in JavaScript?

An event is a signal from the browser that something happened. A user clicked a button, typed a letter, pressed a key, or the page finished loading. Think of a doorbell: nothing happens until someone presses it, and then you go to the door. In JavaScript, you tell an element: "When this event happens, run this function." That function is called an event listener or event handler.

You attach a listener with addEventListener(). It takes two main arguments: the event name as a string, and the function to run.

<!DOCTYPE html>
<html lang="en">
  <body>
    <button id="like">Like</button>
    <p id="count">0 likes</p>

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

      likeButton.addEventListener("click", () => {
        likes += 1;
        countText.textContent = `${likes} likes`;
        console.log("Clicked!", likes);
      });
    </script>
  </body>
</html>
Clicked! 1
Clicked! 2

Each time you click the button, the arrow function runs. It adds one to likes and updates the paragraph. Notice that we pass the function itself; we do not call it. Writing addEventListener("click", update()) would run update once right away, which is a common mistake.

Common events: click, input, change, keydown and mouseenter

Here are the events you will use most often:

EventWhen it fires
clickThe element is clicked or tapped (also Enter/Space on a focused button)
inputThe value of a text field changes, on every keystroke
changeThe value is committed: a checkbox is ticked, a select changes, or a text field loses focus
keydownA key is pressed down
mouseenterThe mouse pointer moves onto the element
mouseleaveThe mouse pointer leaves the element
<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      .tip-box { padding: 12px; background: #eef; }
      .active { background: #cde; }
    </style>
  </head>
  <body>
    <label for="name">Your name</label>
    <input id="name" type="text">
    <p id="preview">Hello, stranger!</p>

    <label for="size">Size</label>
    <select id="size">
      <option>Small</option>
      <option>Large</option>
    </select>

    <div class="tip-box" id="tip">Hover over me</div>

    <script>
      const nameInput = document.querySelector("#name");
      const preview = document.querySelector("#preview");
      const sizeSelect = document.querySelector("#size");
      const tip = document.querySelector("#tip");

      nameInput.addEventListener("input", () => {
        preview.textContent = `Hello, ${nameInput.value || "stranger"}!`;
      });

      sizeSelect.addEventListener("change", () => {
        console.log("Size chosen:", sizeSelect.value);
      });

      document.addEventListener("keydown", (event) => {
        if (event.key === "Escape") {
          nameInput.value = "";
          preview.textContent = "Hello, stranger!";
        }
      });

      tip.addEventListener("mouseenter", () => tip.classList.add("active"));
      tip.addEventListener("mouseleave", () => tip.classList.remove("active"));
    </script>
  </body>
</html>
Size chosen: Large

As you type your name, the greeting updates live thanks to input. Picking "Large" logs the choice once with change. Pressing Escape anywhere on the page clears the field; event.key tells you which key was pressed (you will learn more about the event object in the next lesson). Hovering the box changes its colour.

Tip: For things that should feel instant, like live search or a character counter, use input. For things that should happen once the user has decided, like a checkbox or a select menu, use change.

Running code when the page is ready: DOMContentLoaded

The DOMContentLoaded event fires on document when the browser has finished building the DOM. Code inside it can safely select any element.

document.addEventListener("DOMContentLoaded", () => {
  const heading = document.querySelector("h1");
  console.log("DOM ready:", heading.textContent);
});
// Output: DOM ready: Welcome

If you load your script with defer, the DOM is already ready when your code runs, so you usually do not need this event. It is still useful for scripts without defer, and you will see it in many codebases.

How to remove an event listener

Sometimes a listener should stop working, for example a "claim your free gift" button that works only once. Use removeEventListener() with the same event name and the same function. That means you must store the function in a variable; an anonymous arrow function cannot be removed because you have no name to refer to it.

<!DOCTYPE html>
<html lang="en">
  <body>
    <button id="gift">Claim free gift</button>

    <script>
      const giftButton = document.querySelector("#gift");

      const claimGift = () => {
        console.log("Gift claimed!");
        giftButton.textContent = "Gift claimed";
        giftButton.disabled = true;
        giftButton.removeEventListener("click", claimGift);
      };

      giftButton.addEventListener("click", claimGift);
    </script>
  </body>
</html>
Gift claimed!

After the first click, the listener removes itself and the button is disabled. For this "only once" case there is also a shortcut: giftButton.addEventListener("click", claimGift, { once: true }) removes the listener automatically after it runs.

Why not use onclick attributes?

In older code you will see HTML like <button onclick="doSomething()">. This mixes JavaScript into your HTML, only allows one handler per event, and needs the function to be global. Many websites also block inline code with a security setting called a Content Security Policy, so it may not run at all.

Common mistake: Writing onclick attributes in HTML. Keep HTML for structure and attach behaviour in your JavaScript file with addEventListener. You can add several listeners to the same event, and remove them when needed.

Practice exercise

Build a character counter for a tweet-like message. Add a <textarea> and a <p> that shows "0 / 140". On every input event, update the counter with the current length. When the length goes over 140, add a class too-long to the counter that makes the text red; remove it again when the text is short enough.

Hint: textarea.value.length gives the number of characters, and classList.toggle("too-long", length > 140) adds or removes the class based on a condition.

Key takeaways

  • An event is a signal that something happened; addEventListener(eventName, function) reacts to it.
  • Pass the function itself, not the result of calling it.
  • Use input for live updates, change for final choices, keydown for keys and mouseenter/mouseleave for hover.
  • To remove a listener, pass the same named function to removeEventListener, or use { once: true }.
  • Avoid onclick attributes in HTML; use addEventListener in your script.

Frequently asked questions

What is the difference between input and change events?

input fires every time the value changes, on each keystroke. change fires when the user commits the value, for example when a text field loses focus or a checkbox is ticked.

Can I add two listeners for the same event?

Yes. With addEventListener you can attach as many listeners as you like, and they run in the order you added them.

Do click events work on phones?

Yes. A tap on a touch screen fires a click event, so the same code works on desktop and mobile.

Open this lesson on its own page ↗

Module 10 of 16

Intermediate: The Browser Object Model (BOM)

Lesson 10.1 6 min read

JavaScript Window Object and Screen: The Browser Object Model Explained

The JavaScript window object is the browser tab your code runs in. In this lesson you will learn what the Browser Object Model (BOM) is, how to read the window and screen size, react to resizing, scroll the page and check media queries.

What is the BOM and how is it different from the DOM?

The BOM (Browser Object Model) is the set of objects the browser gives you to talk to the browser itself, not to the page content. The DOM (Document Object Model), which you met in the DOM module, is about the page: headings, buttons, paragraphs.

A simple comparison: if a web page is a letter, the DOM is the text on the paper, and the BOM is the envelope, the post office and the desk the letter sits on. The BOM tells you how big the window is, what the current address (URL) is, what language the user speaks, and lets you go back in history.

The main BOM objects are:

ObjectWhat it gives you
windowThe browser tab itself: size, scrolling, timers, popups
window.screenInformation about the user's physical screen
window.locationThe current URL, and ways to change it
window.historyThe back and forward history of the tab
window.navigatorInformation about the browser and device
documentThe page itself (this is the DOM)

Notice that document lives inside window too. The DOM is actually one part of the bigger window object.

Note: There is no official "BOM standard". The name is just a handy label for these browser objects. Today they are all described in the HTML Living Standard and other web standards, and they work the same way in all modern browsers.

Why is window called the global object?

In a browser, window is the global object. That means everything that is available "everywhere" in your script is really a property of window. You have been using it all along without writing its name.

console.log(window.document === document);
// Output: true

window.console.log("Hello from window.console");
// Output: Hello from window.console

console.log(typeof window.setTimeout);
// Output: function

var oldStyle = "I am on window";
let modern = "I am not on window";
console.log(window.oldStyle);
// Output: I am on window
console.log(window.modern);
// Output: undefined

Here document, console, setTimeout and alert are all properties of window, so you can write them with or without window. in front. The last part shows an important detail: old-style var variables at the top level become properties of window, but let and const do not. This is one more reason to use let and const: they do not clutter the global object.

Common mistake: Naming your own top-level variable name, status or top with var. These already exist on window (for example window.name is always a string), so your value can behave strangely. Use const and more specific names like userName.

How to get the window size with innerWidth and innerHeight

window.innerWidth and window.innerHeight give you the size of the viewport, the visible area of the page inside the browser, in CSS pixels. outerWidth and outerHeight include the browser's toolbars and borders, and you rarely need them.

To react when the user resizes the window (or turns their phone), listen for the resize event on window:

<!DOCTYPE html>
<html lang="en">
<body>
  <p id="size">Resize the window...</p>

  <script>
    const sizeText = document.querySelector("#size");

    const showSize = () => {
      const width = window.innerWidth;
      const height = window.innerHeight;
      const layout = width < 600 ? "phone layout" : "desktop layout";
      sizeText.textContent = `${width} x ${height} px (${layout})`;
    };

    showSize();                                   // show it once at the start
    window.addEventListener("resize", showSize);  // and again on every resize
  </script>
</body>
</html>

When you open this page, you see something like "1280 x 720 px (desktop layout)". Drag the window narrower and the text updates live. We call showSize() once immediately, because the resize event only fires when the size changes, not when the page loads.

Tip: The resize event can fire dozens of times per second while dragging. Keep the handler small. In the Performance lesson you will learn to "debounce" it so heavy work runs only once the user stops resizing.

How to scroll with JavaScript: scrollTo, scrollBy and scrollY

window.scrollY tells you how many pixels the page is scrolled down from the top (and scrollX from the left). To move the page yourself, use:

  • window.scrollTo(x, y) or window.scrollTo({ top, left, behavior }) scrolls to an exact position.
  • window.scrollBy(x, y) or window.scrollBy({ top, behavior }) scrolls by an amount from where you are now.

Here is a classic "Back to top" button that appears only after you scroll down:

<!DOCTYPE html>
<html lang="en">
<body style="height: 3000px; font-family: sans-serif;">
  <h1>A very long article</h1>
  <button id="toTop" hidden style="position: fixed; bottom: 20px; right: 20px;">Back to top</button>

  <script>
    const toTopButton = document.querySelector("#toTop");

    window.addEventListener("scroll", () => {
      // show the button after 400px of scrolling
      toTopButton.hidden = window.scrollY < 400;
    });

    toTopButton.addEventListener("click", () => {
      window.scrollTo({ top: 0, behavior: "smooth" });
    });
  </script>
</body>
</html>

Scroll down and a button appears in the bottom-right corner. Click it and the page glides back to the top. behavior: "smooth" gives the smooth animation; leave it out (or use "instant") to jump straight there. Try window.scrollBy({ top: 300, behavior: "smooth" }) in the console to move down one "step".

How to use window.screen and devicePixelRatio

window.screen describes the user's physical monitor, not the browser window:

  • screen.width and screen.height are the full screen size.
  • screen.availWidth and screen.availHeight are the space left after the taskbar or dock.
  • window.devicePixelRatio tells you how many real device pixels make one CSS pixel. A normal monitor gives 1; most phones and "Retina" screens give 2 or 3.
console.log(`Screen: ${screen.width} x ${screen.height}`);
console.log(`Available: ${screen.availWidth} x ${screen.availHeight}`);
console.log(`Window: ${window.innerWidth} x ${window.innerHeight}`);
console.log(`Pixel ratio: ${window.devicePixelRatio}`);
// Output (example on a laptop):
// Screen: 1440 x 900
// Available: 1440 x 875
// Window: 1280 x 720
// Pixel ratio: 2

A pixel ratio of 2 means the screen is sharp, so you might load a bigger image for it. For normal layout decisions, though, use the window size, not the screen size: the user's window is often smaller than their screen.

How to check a CSS media query in JavaScript with matchMedia

window.matchMedia() lets JavaScript ask the same questions as CSS media queries. It returns an object with a matches property (true or false) and a change event you can listen to.

const smallScreen = window.matchMedia("(max-width: 600px)");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");

console.log("Small screen?", smallScreen.matches);
console.log("Dark mode?", prefersDark.matches);
// Output (example): Small screen? false
// Output (example): Dark mode? true

smallScreen.addEventListener("change", (event) => {
  console.log(event.matches ? "Switched to small layout" : "Switched to large layout");
});

The change event fires only when the answer flips, for example when the window crosses 600px. That makes matchMedia much lighter than checking innerWidth on every resize. It also keeps your JavaScript breakpoints in sync with your CSS ones. You can read more on MDN's matchMedia page.

Practice exercise

Build a small "reading progress" page. Make the body very tall (for example height: 4000px). Add a paragraph fixed at the top that shows "You have read 35% of this page". Update it on every scroll event. Then add a second line that says "Phone" or "Desktop" using matchMedia("(max-width: 600px)") and its change event.

Hint: the total scrollable distance is document.documentElement.scrollHeight - window.innerHeight. Divide window.scrollY by that number, multiply by 100 and use Math.round().

Key takeaways

  • The BOM is the set of browser objects (window, screen, location, history, navigator); the DOM (document) is the page content inside it.
  • window is the global object: alert, setTimeout and document are all its properties.
  • innerWidth and innerHeight give the viewport size; listen to resize to react to changes.
  • Use scrollY to read the scroll position and scrollTo / scrollBy with behavior: "smooth" to move it.
  • matchMedia checks CSS media queries from JavaScript and fires change only when the result flips.

Open this lesson on its own page ↗

Module 11 of 16

Intermediate: Useful Built-in Features

Lesson 11.1 5 min read

Dates and Timers: Date, setTimeout and setInterval

JavaScript has built-in tools for time. In this lesson you will learn to work with dates using the Date object, format them nicely, and run code later with setTimeout and setInterval.

How to create a date in JavaScript

A Date object stores one moment in time. new Date() with no arguments gives you right now. You can also create a specific date in a few ways:

const now = new Date();
console.log(now);
// Output (yours will differ): Thu Oct 01 2026 09:30:12 GMT+0100 (Western European Summer Time)

const birthday = new Date(2026, 0, 15);
console.log(birthday.toDateString());
// Output: Thu Jan 15 2026

const meeting = new Date("2026-03-20T14:30:00");
console.log(meeting.getHours(), meeting.getMinutes());
// Output: 14 30

With numbers, the order is year, month, day (and optionally hours, minutes, seconds). Be careful: months start at 0, so 0 is January and 11 is December. Days of the month start at 1 as normal. With a string, use the ISO format YYYY-MM-DDTHH:mm:ss, which every browser understands.

Common mistake: Writing new Date(2026, 1, 15) and expecting January 15. Month 1 is February. This trips up almost everyone at least once.

Getting parts of a date

Date objects have "getter" methods to read each part:

MethodReturns
getFullYear()The year, e.g. 2026
getMonth()The month, 0 to 11
getDate()The day of the month, 1 to 31
getDay()The day of the week, 0 (Sunday) to 6 (Saturday)
getHours(), getMinutes(), getSeconds()The time parts
getTime()Milliseconds since 1 January 1970 (UTC)
const order = new Date(2026, 0, 15, 9, 5);
const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

console.log(order.getFullYear());
// Output: 2026
console.log(order.getMonth() + 1);
// Output: 1
console.log(weekdays[order.getDay()]);
// Output: Thu
console.log(`${order.getHours()}:${String(order.getMinutes()).padStart(2, "0")}`);
// Output: 9:05

We add 1 to the month for a human-friendly number, and use padStart so the minutes show as 05, not 5.

Formatting dates with toLocaleDateString and Intl.DateTimeFormat

Building date strings by hand is tiring, and every country writes dates differently. The browser can do it for you. toLocaleDateString() takes a locale (a language and region code, like "en-US" or "de-DE") and options.

const launch = new Date(2026, 0, 15);

console.log(launch.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" }));
// Output: Thursday, January 15, 2026

console.log(launch.toLocaleDateString("de-DE", { weekday: "long", year: "numeric", month: "long", day: "numeric" }));
// Output: Donnerstag, 15. Januar 2026

const short = new Intl.DateTimeFormat("en-GB", { dateStyle: "medium" });
console.log(short.format(launch));
// Output: 15 Jan 2026

const timeOnly = new Intl.DateTimeFormat("en-US", { timeStyle: "short" });
console.log(timeOnly.format(new Date(2026, 0, 15, 18, 45)));
// Output: 6:45 PM

Intl.DateTimeFormat creates a reusable formatter. It is a good choice when you format many dates the same way, like every row in a table. If you leave out the locale (undefined), the browser uses the visitor's own language settings.

Date math basics

Because a date is really a number of milliseconds, you can subtract two dates. The result is a difference in milliseconds, which you can turn into days. To move a date forward, change one part with a "setter" like setDate(); JavaScript handles the month overflow for you.

const MS_PER_DAY = 1000 * 60 * 60 * 24;

const today = new Date(2026, 9, 1);
const christmas = new Date(2026, 11, 25);
const daysLeft = Math.round((christmas - today) / MS_PER_DAY);
console.log(`${daysLeft} days until Christmas`);
// Output: 85 days until Christmas

const trialStart = new Date(2026, 0, 15);
const trialEnd = new Date(trialStart);
trialEnd.setDate(trialEnd.getDate() + 30);
console.log(trialEnd.toDateString());
// Output: Sat Feb 14 2026

We use Math.round because daylight saving time can make one day 23 or 25 hours long. new Date(trialStart) makes a copy, so the original date stays unchanged.

Note: A newer, much friendlier date API called Temporal is arriving in browsers. It fixes many Date problems, such as zero-based months and time zone confusion. Check browser support before you rely on it; Date works everywhere today.

Running code later: setTimeout, setInterval and clearInterval

setTimeout(fn, ms) runs a function once after a delay in milliseconds (1000 ms = 1 second). setInterval(fn, ms) runs it again and again at that interval. Both return an ID number, which you pass to clearTimeout() or clearInterval() to cancel.

console.log("Order placed");

const reminder = setTimeout(() => {
  console.log("Don't forget to check your email!");
}, 2000);

// Change your mind? Cancel it before it runs:
// clearTimeout(reminder);

console.log("This line runs first");
Order placed
This line runs first
Don't forget to check your email!

The last line appears after two seconds. JavaScript does not stop and wait; it schedules the function and carries on.

Example: a countdown timer

<!DOCTYPE html>
<html lang="en">
  <body>
    <p>Offer ends in <strong id="timer">10</strong> seconds</p>
    <button id="stop">Stop timer</button>

    <script>
      const timerText = document.querySelector("#timer");
      let secondsLeft = 10;

      const intervalId = setInterval(() => {
        secondsLeft -= 1;
        timerText.textContent = secondsLeft;

        if (secondsLeft === 0) {
          clearInterval(intervalId);
          timerText.textContent = "0 - the offer has ended";
          console.log("Countdown finished");
        }
      }, 1000);

      document.querySelector("#stop").addEventListener("click", () => {
        clearInterval(intervalId);
        console.log(`Stopped with ${secondsLeft} seconds left`);
      });
    </script>
  </body>
</html>
Countdown finished

Every second, the number goes down by one. At zero we call clearInterval so the timer stops; without it, the count would go into negative numbers. The button stops the timer early.

Tip: Timers are not perfectly exact. The browser may run them a little late, especially in background tabs. For a precise clock, calculate the time left from the end date on each tick instead of counting ticks.

Practice exercise

Show a live clock on a page. Every second, update a <p> with the current time formatted by Intl.DateTimeFormat with { timeStyle: "medium" }. Below it, show how many days are left until your next birthday.

Hint: Create the formatter once, outside setInterval, and call formatter.format(new Date()) inside it.

Key takeaways

  • new Date() gives the current moment; in new Date(year, month, day) the month starts at 0.
  • Use getters like getFullYear() and getDay() to read parts of a date.
  • Format dates for people with toLocaleDateString or Intl.DateTimeFormat.
  • Subtracting dates gives milliseconds; setDate() moves a date forward or back.
  • setTimeout runs code once later, setInterval repeats it, and clearInterval stops it.

Open this lesson on its own page ↗

Module 12 of 16

Advanced: Asynchronous JavaScript

Lesson 12.1 6 min read

How Asynchronous JavaScript Works: Callbacks and the Event Loop

Asynchronous JavaScript lets your code start a slow task, keep working, and handle the result later. This lesson explains callbacks, the call stack and the event loop.

What is the difference between synchronous and asynchronous code?

Synchronous code runs one line at a time, in order. Each line must finish before the next one starts. Asynchronous code starts a task now and finishes it later, so the rest of your code does not have to wait.

Think of a restaurant. A synchronous waiter takes your order, walks to the kitchen, stands there until your food is cooked, brings it to you, and only then takes the next order. Everyone else waits. An asynchronous waiter takes your order, gives it to the kitchen, and goes to serve other tables. When the kitchen rings the bell, the waiter brings your food. The kitchen is doing the slow work in the background.

JavaScript in the browser works like the second waiter. It has only one main thread, which means it can only do one thing at a time. Slow jobs like timers, network requests and waiting for clicks are handed to the browser, and JavaScript keeps going.

console.log("1. Take order from table 1");

setTimeout(() => {
  console.log("3. Food for table 1 is ready");
}, 2000);

console.log("2. Take order from table 2");

// Output:
// 1. Take order from table 1
// 2. Take order from table 2
// 3. Food for table 1 is ready   (after 2 seconds)

setTimeout hands the timer to the browser and returns at once. That is why line 2 prints before line 3, even though it comes later in the code.

How does the JavaScript event loop work?

To understand the order above, you need four pieces:

  • Call stack: the list of functions JavaScript is running right now. When a function is called, it goes on top of the stack. When it returns, it is removed. Like a stack of plates, the last one in is the first one out.
  • Web APIs: tools the browser gives you, like setTimeout, fetch and addEventListener. They do the waiting outside of JavaScript.
  • Task queue (also called the macrotask queue): when a timer finishes or a click happens, the callback waits in this line.
  • Microtask queue: a second, more urgent line. Promise callbacks (you will meet promises in the next lesson) wait here.

The event loop is a simple helper that keeps checking: "Is the call stack empty? If yes, run all waiting microtasks first, then take one task from the task queue."

   Your code                Browser (Web APIs)
 +-------------+          +---------------------+
 | Call stack  | -------> | setTimeout, fetch,  |
 |             |          | click listeners ... |
 +-------------+          +---------------------+
        ^                            |
        |                            v
        |              +---------------------------+
        |              | Microtask queue           |  <- promises (run first)
        |              +---------------------------+
        |              | Task queue                |  <- timers, events
        |              +---------------------------+
        |                            |
        +-------- Event loop --------+
     "Stack empty? Run all microtasks, then one task."

Here is a small test that shows microtasks run before tasks, even when the timer has a delay of 0:

console.log("A: start");

setTimeout(() => console.log("D: timeout (task)"), 0);

Promise.resolve().then(() => console.log("C: promise (microtask)"));

console.log("B: end");

// Output:
// A: start
// B: end
// C: promise (microtask)
// D: timeout (task)

First, all the normal code runs (A and B). The stack is now empty, so the event loop runs the microtask (C). Only then does it take the timer task (D). A delay of 0 means "as soon as possible", not "right now".

Note: Because there is only one main thread, a long loop in your code blocks everything: clicks, timers and even page drawing. Keep heavy work short, or split it into smaller pieces.

What is a callback function in JavaScript?

A callback is a function you pass to another function, so it can be called later. You have already used callbacks with addEventListener and setTimeout. Callbacks were the first way JavaScript handled async work.

function loadUser(id, callback) {
  console.log(`Loading user ${id}...`);
  setTimeout(() => {
    const user = { id: id, name: "Maya" };
    callback(user); // call the function we were given
  }, 1000);
}

loadUser(7, (user) => {
  console.log(`Hello, ${user.name}!`);
});

// Output:
// Loading user 7...
// Hello, Maya!   (after 1 second)

loadUser pretends to fetch a user from a server. It cannot return the user, because the data is not ready yet when the function ends. So instead, it calls the callback when the data arrives.

What is callback hell?

Callbacks work well for one step. But real apps often need several steps in order: load the user, then their orders, then the details of the first order. With callbacks, each step goes inside the one before it.

function getUser(id, cb) { setTimeout(() => cb({ id, name: "Maya" }), 500); }
function getOrders(user, cb) { setTimeout(() => cb([{ id: 101 }, { id: 102 }]), 500); }
function getOrderDetails(order, cb) { setTimeout(() => cb({ id: order.id, total: 49.99 }), 500); }

getUser(7, (user) => {
  getOrders(user, (orders) => {
    getOrderDetails(orders[0], (details) => {
      console.log(`${user.name}'s first order costs $${details.total}`);
    });
  });
});

// Output (after 1.5 seconds):
// Maya's first order costs $49.99

The code moves further and further to the right. This shape is called callback hell or the "pyramid of doom". It is hard to read, and error handling is painful, because you need to check for errors at every level. Promises and async/await, which you will learn next, fix this problem.

Common mistake: Trying to return a value from inside a setTimeout callback and expecting the outer function to give it back. The outer function has already finished. Use a callback, a promise or await instead.

Practice exercise

Without running it first, write down the order in which this code prints its letters. Then run it in the console to check your answer.

console.log("W");
setTimeout(() => console.log("X"), 0);
Promise.resolve().then(() => console.log("Y"));
console.log("Z");

Hint: normal code first, then microtasks (promises), then tasks (timers).

Key takeaways

  • JavaScript has one main thread, so slow jobs are handed to browser Web APIs.
  • The call stack runs your code; finished async jobs wait in queues.
  • The event loop runs all microtasks (promises) before the next task (timers, events).
  • A callback is a function passed in to be called later.
  • Deeply nested callbacks create "callback hell", which promises solve.

Frequently asked questions

Is JavaScript single-threaded?

Yes. Your JavaScript code runs on one main thread. The browser itself uses other threads for timers and network requests, which is why JavaScript can still do async work.

Why does setTimeout with 0 milliseconds not run immediately?

The callback must wait in the task queue until the call stack is empty and all microtasks are done. Zero only means "no extra delay".

Are callbacks still used today?

Yes, for events (addEventListener) and array methods like map. For one-time async results, modern code uses promises and async/await instead.

Open this lesson on its own page ↗

Module 13 of 16

Advanced: Modern JavaScript Patterns

Lesson 13.1 5 min read

JavaScript Modules: import and export

JavaScript modules let you split your code into small files that share code with import and export. Learn named and default exports, import paths and dynamic import().

Why use JavaScript modules?

When a project grows, one big script.js file becomes hard to read and easy to break. Modules let you split code into many small files, each with one job, like rooms in a house. Each file decides what it shares (export) and what it needs from other files (import).

Modules also fix an old problem. With normal scripts, every top-level variable is global, so two files can accidentally use the same name and overwrite each other. In a module, variables are private to that file unless you export them.

How to load a module with type="module"

To use modules in the browser, add type="module" to your script tag:

<script type="module" src="main.js"></script>

Module scripts behave a little differently from normal scripts:

  • They are deferred. They wait until the HTML is fully parsed, so you can use document.querySelector without worrying about where the tag is placed.
  • They are always in strict mode. Strict mode turns silent mistakes into real errors. For example, using a variable you never declared throws an error instead of creating a global.
  • They run only once, even if many files import them.
  • They need a web server. Opening the HTML file directly (file://) blocks modules for safety. Use a local server, such as the VS Code "Live Server" extension or XAMPP.

Note: Top-level this is undefined in a module, and top-level await is allowed, as you saw in the async/await lesson.

What is the difference between named and default exports?

A named export shares something by its name. A file can have many named exports. You import them with curly braces and the exact same names.

A default export is the one "main" thing a file shares. A file can have only one. You import it without curly braces and can give it any name you like.

// file: math.js
export const TAX_RATE = 0.2;               // named export

export function addTax(price) {            // named export
  return price * (1 + TAX_RATE);
}

export default function formatPrice(n) {   // default export
  return `$${n.toFixed(2)}`;
}
// file: main.js
import formatPrice, { addTax, TAX_RATE } from "./math.js";

console.log(TAX_RATE);                 // Output: 0.2
console.log(formatPrice(addTax(10)));  // Output: $12.00

You can rename a named import with as, which helps when two files use the same name: import { addTax as withTax } from "./math.js";. You can also import everything as one object: import * as math from "./math.js";, then use math.addTax(10).

Tip: Many teams prefer named exports, because the names stay the same in every file. That makes searching and auto-import in your editor work better.

How do import paths work?

In the browser, the path must point to a real file:

  • "./math.js" means "math.js in the same folder as this file".
  • "../utils/date.js" means "go up one folder, then into utils".
  • "/js/math.js" means "from the root of the website".

Always include the .js extension in the browser. A bare name like import ... from "lodash" does not work in a plain browser page; it needs a build tool (like Vite) or an import map, which are outside this lesson.

A small multi-file module example

Let's build a tiny shopping cart with three files. Put them in the same folder and open index.html through a local server.

<!-- file: index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Module cart</title>
  <script type="module" src="app.js"></script>
</head>
<body>
  <h1>My cart</h1>
  <ul id="cart"></ul>
  <p id="total"></p>
</body>
</html>
// file: cart.js
const items = []; // private: other files cannot touch it directly

export function addItem(name, price) {
  items.push({ name, price });
}

export function getItems() {
  return [...items]; // return a copy
}

export function getTotal() {
  return items.reduce((sum, item) => sum + item.price, 0);
}
// file: app.js
import { addItem, getItems, getTotal } from "./cart.js";

addItem("Notebook", 4.5);
addItem("Pen", 1.25);

const list = document.querySelector("#cart"); // works: modules are deferred
getItems().forEach((item) => {
  const li = document.createElement("li");
  li.textContent = `${item.name}: $${item.price.toFixed(2)}`;
  list.append(li);
});

document.querySelector("#total").textContent = `Total: $${getTotal().toFixed(2)}`;

The page shows "Notebook: $4.50", "Pen: $1.25" and "Total: $5.75". The script is in the <head>, but it still finds the elements because modules wait for the HTML. The items array is hidden inside cart.js, so the only way to change it is through the exported functions.

How to use dynamic import()

Normal import statements load at the start. Sometimes you only need code later, for example a big chart library when the user clicks "Show chart". import() used like a function loads a module on demand and returns a promise.

// file: app.js (add this at the end)
const button = document.createElement("button");
button.textContent = "Show total with tax";
document.body.append(button);

button.addEventListener("click", async () => {
  const math = await import("./math.js"); // loaded only on first click
  const withTax = math.addTax(getTotal()); // named exports are properties
  console.log(math.default(withTax));      // the default export is called "default"
  // Output: $6.90
});

Put the math.js file from earlier in the same folder. The first click downloads math.js; later clicks reuse it. This keeps the first page load small and fast.

Common mistake: Forgetting type="module" on the script tag. You will see "Cannot use import statement outside a module". Add the attribute and serve the page from a local server.

Practice exercise

Create a file greetings.js with a named export sayHello(name) and a default export sayBye(name). Import both in main.js and print the messages to the console. Then add a button that uses import() to load a third file, jokes.js, only when clicked.

Hint: import the default without braces and the named one with braces: import sayBye, { sayHello } from "./greetings.js";.

Key takeaways

  • Modules split code into files; each file's variables are private unless exported.
  • Use <script type="module">; modules are deferred, strict and need a web server.
  • A file can have many named exports (imported with { }) and one default export.
  • Browser import paths start with ./, ../ or / and include .js.
  • import() loads a module on demand and returns a promise.

Open this lesson on its own page ↗

Module 14 of 16

Advanced: Advanced Language Features

Lesson 14.1 5 min read

Map, Set, WeakMap and WeakSet in JavaScript

JavaScript Map, Set, WeakMap and WeakSet are special collections. Learn when a Map beats a plain object, how a Set removes duplicates, and what weak collections are for.

What is a Map in JavaScript?

A Map is a collection of key–value pairs, like an object. Think of it as a row of labelled boxes: you put a value in a box and stick a label (the key) on it.

The big difference is that a Map key can be any value: a string, a number, an object, even a DOM element. In a plain object, keys are always turned into strings.

const prices = new Map();

prices.set('apple', 0.5);     // add a key and a value
prices.set('banana', 0.25);
prices.set('cherry', 3);

console.log(prices.get('apple'));    // Output: 0.5
console.log(prices.has('banana'));   // Output: true
console.log(prices.size);            // Output: 3

prices.delete('cherry');             // remove one entry

for (const [fruit, price] of prices) {
  console.log(`${fruit} costs $${price}`);
}
// Output:
// apple costs $0.5
// banana costs $0.25

set() adds or updates an entry, get() reads it, has() checks if a key exists, and size tells you how many entries there are. A Map also remembers the order you added things, so the for...of loop gives you entries in that order.

Map vs object: which one should you use?

Both store key–value pairs, so how do you choose? Here is a simple guide.

QuestionPlain objectMap
What can a key be?Strings (and symbols)Any value
How do I count entries?Object.keys(obj).lengthmap.size
Can I loop it directly?No, use Object.entries()Yes, with for...of
Good for JSON?Yes, works with JSON.stringifyNo, convert it first
Best useFixed "shape" data like a user profileLookups that grow and shrink a lot

Use an object for a record with known fields, like { name, email, age }. Use a Map when you add and remove many keys while the program runs, or when your keys are not strings.

// Count how many times each word appears
const text = 'the cat and the dog and the bird';
const counts = new Map();

for (const word of text.split(' ')) {
  counts.set(word, (counts.get(word) ?? 0) + 1);
}

console.log(counts.get('the'));   // Output: 3
console.log([...counts]);
// Output: [["the",3],["cat",1],["and",2],["dog",1],["bird",1]]

For each word, we read the current count (or 0 if the word is new) and add one. The spread [...counts] turns the Map into an array of pairs so you can see it all.

Using a Set for unique values

A Set is a collection where every value appears only once. It is like a guest list: if you write the same name twice, the person still gets one seat.

const tags = new Set();

tags.add('javascript');
tags.add('css');
tags.add('javascript');  // ignored, it is already there

console.log(tags.size);              // Output: 2
console.log(tags.has('css'));        // Output: true
tags.delete('css');
console.log([...tags]);              // Output: ["javascript"]

How to remove duplicates from an array

The most popular trick with a Set is removing duplicates from an array in one line.

const emails = ['ana@mail.com', 'ben@mail.com', 'ana@mail.com', 'cara@mail.com'];

const uniqueEmails = [...new Set(emails)];

console.log(uniqueEmails);
// Output: ["ana@mail.com", "ben@mail.com", "cara@mail.com"]

new Set(emails) keeps only the first copy of each value. The spread [...] turns the Set back into a normal array.

Common mistake: A Set compares objects by reference, not by content. new Set([{ id: 1 }, { id: 1 }]) has a size of 2, because they are two different objects that only look the same.

Set methods: union, intersection and difference

Modern browsers have built-in methods to combine Sets. Imagine two clubs and you want to compare their members.

const chessClub = new Set(['Ana', 'Ben', 'Cara']);
const musicClub = new Set(['Ben', 'Dan', 'Cara']);

console.log([...chessClub.union(musicClub)]);
// Output: ["Ana", "Ben", "Cara", "Dan"]   (in either club)

console.log([...chessClub.intersection(musicClub)]);
// Output: ["Ben", "Cara"]   (in both clubs)

console.log([...chessClub.difference(musicClub)]);
// Output: ["Ana"]   (only in the chess club)

console.log(new Set(['Ben']).isSubsetOf(chessClub));
// Output: true

There is also symmetricDifference() (in one club but not both), isSupersetOf() and isDisjointFrom().

Note: These Set methods arrived in all major browsers in 2024 (Chrome 122, Firefox 127, Safari 17). If you must support older browsers, use the array way instead, for example [...a].filter(x => b.has(x)) for an intersection.

What are WeakMap and WeakSet for?

A WeakMap is a Map where the keys must be objects, and it holds them "weakly". That means: if nothing else in your program uses that object any more, JavaScript can throw it away (this cleanup is called garbage collection), and the WeakMap entry disappears too.

A real-life comparison: a coat check that automatically throws away the ticket when the coat's owner leaves the building forever. You never have to clean up by hand.

A common use is storing extra private data about DOM elements without changing the elements themselves.

const clickCounts = new WeakMap();

document.querySelectorAll('button').forEach((button) => {
  clickCounts.set(button, 0);

  button.addEventListener('click', () => {
    clickCounts.set(button, clickCounts.get(button) + 1);
    console.log(`Clicked ${clickCounts.get(button)} times`);
  });
});
// Output after clicking a button twice: Clicked 1 times, Clicked 2 times

If a button is removed from the page and no other code points to it, its entry in clickCounts can be cleaned up automatically. That avoids memory leaks (memory that is never freed).

A WeakSet is the same idea for a Set: it holds objects only, for example to remember "which elements did I already set up?". Weak collections cannot be looped over and have no size, because their contents can vanish at any moment.

Practice exercise

You have an array of product categories from a shop: ['shoes', 'hats', 'shoes', 'bags', 'hats', 'socks']. First, make an array with no duplicates. Then use a Map to count how many times each category appears, and log the result as shoes: 2 and so on.

Hint: use [...new Set(array)] for the first part, and map.get(key) ?? 0 for the counting part.

Key takeaways

  • A Map stores key–value pairs where keys can be any value, keeps insertion order, and has a handy size.
  • Use plain objects for fixed records and JSON; use a Map for frequently changing lookups.
  • A Set stores unique values, and [...new Set(array)] removes duplicates.
  • Newer Set methods like union() and intersection() work in all modern browsers since 2024.
  • WeakMap and WeakSet hold objects weakly, so data about removed objects is cleaned up automatically.

Open this lesson on its own page ↗

Module 15 of 16

Advanced: Browser APIs and Performance

Lesson 15.1 5 min read

Useful Browser APIs: Intersection Observer, Clipboard and Geolocation

Browser APIs like Intersection Observer, ResizeObserver, the Clipboard API and Geolocation add powerful features to your pages. Learn lazy loading, copy buttons and location with permissions.

What is the Intersection Observer API?

The Intersection Observer tells you when an element enters or leaves the visible part of the screen (the viewport). Think of a security guard at a door who calls you when someone walks in, so you don't have to keep looking yourself.

The old way was to listen to the scroll event and measure every element on every scroll. That is slow. An Intersection Observer lets the browser do the watching efficiently.

Reveal-on-scroll and lazy loading with IntersectionObserver

Let's fade in cards as they scroll into view.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Reveal on scroll</title>
  <style>
    .card { margin: 60vh auto; width: 300px; padding: 2rem; background: #eef; opacity: 0; transform: translateY(40px); transition: opacity 0.6s, transform 0.6s; }
    .card.visible { opacity: 1; transform: none; }
  </style>
</head>
<body>
  <div class="card">Feature one</div>
  <div class="card">Feature two</div>
  <div class="card">Feature three</div>

  <script>
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.classList.add('visible');   // show the card
          observer.unobserve(entry.target);        // stop watching it
        }
      });
    }, { threshold: 0.2 });   // fire when 20% of the card is visible

    document.querySelectorAll('.card').forEach((card) => observer.observe(card));
  </script>
</body>
</html>

new IntersectionObserver(callback, options) creates the observer. observe() starts watching an element. The callback receives a list of entries; entry.isIntersecting is true when the element is on screen. We add the class once and call unobserve() because we only want the effect one time. As you scroll down, each card fades and slides up.

The same idea works for lazy loading: keep the real image address in a data-src attribute and copy it to src when the image comes close.

const lazyObserver = new IntersectionObserver((entries, obs) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;
    const img = entry.target;
    img.src = img.dataset.src;   // start downloading now
    obs.unobserve(img);
  }
}, { rootMargin: '200px' });     // start 200px before it reaches the screen

document.querySelectorAll('img[data-src]').forEach((img) => lazyObserver.observe(img));

rootMargin: '200px' makes the observer react a little early, so the image is often ready by the time the user sees it.

Tip: For plain images, you don't need JavaScript at all: <img src="photo.jpg" loading="lazy" alt="..."> lazy loads natively in all modern browsers. Use an Intersection Observer for custom effects, infinite scroll, or loading other content.

ResizeObserver in brief

A ResizeObserver works the same way, but tells you when an element changes size. It is useful for charts or components that must adapt to their box, not the whole window.

const box = document.querySelector('.card');
const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log(`New width: ${Math.round(entry.contentRect.width)}px`);
  }
});
resizeObserver.observe(box);
// Output: New width: 300px (and again whenever the card's size changes)

How to copy text to the clipboard with JavaScript

The Clipboard API lets you copy text with navigator.clipboard.writeText(). It returns a promise, so we use async/await.

<p>Your invite code: <code id="code">WELCOME-2026</code></p>
<button id="copyBtn">Copy code</button>

<script>
  const copyBtn = document.querySelector('#copyBtn');

  copyBtn.addEventListener('click', async () => {
    const text = document.querySelector('#code').textContent;
    try {
      await navigator.clipboard.writeText(text);
      copyBtn.textContent = 'Copied!';
    } catch (error) {
      copyBtn.textContent = 'Copy failed';
      console.error('Clipboard error:', error);
    }
    setTimeout(() => (copyBtn.textContent = 'Copy code'), 2000);
  });
</script>

When you click the button, the code is copied and the button says "Copied!" for two seconds. The try...catch handles the case where the browser blocks the copy.

Note: The Clipboard API only works on secure pages (HTTPS or localhost) and usually only inside a user action, such as a click. Opening the file directly with file:// may not work.

How to get the user's location with the Geolocation API

The Geolocation API asks the browser for the user's position. The browser always asks the user for permission first, and the user can say no. Your code must handle both answers.

const showLocation = () => {
  if (!('geolocation' in navigator)) {
    console.log('Geolocation is not supported.');
    return;
  }

  navigator.geolocation.getCurrentPosition(
    (position) => {
      const { latitude, longitude, accuracy } = position.coords;
      console.log(`You are near ${latitude.toFixed(3)}, ${longitude.toFixed(3)} (±${Math.round(accuracy)} m)`);
    },
    (error) => {
      if (error.code === error.PERMISSION_DENIED) {
        console.log('You said no. You can type your city instead.');
      } else if (error.code === error.TIMEOUT) {
        console.log('Finding your location took too long.');
      } else {
        console.log('Your location is not available right now.');
      }
    },
    { timeout: 10000 }   // give up after 10 seconds
  );
};

document.querySelector('button')?.addEventListener('click', showLocation);
// Output (after allowing): You are near 51.507, -0.128 (±35 m)

getCurrentPosition() takes a success function, an error function and options. It uses callbacks, not promises. The error code tells you why it failed. Always offer another way, like typing a city, when permission is denied.

You can check the permission state without showing a popup using navigator.permissions.query({ name: 'geolocation' }), which resolves to 'granted', 'denied' or 'prompt'.

A note on notifications and permissions

Notification.requestPermission() asks if your site may show system notifications. The same rules apply as for location: only ask after the user clicks something that clearly needs it, like a "Notify me" button. Asking as soon as the page loads annoys people, and many browsers quietly block those requests. If the answer is 'denied', you cannot ask again from code; the user must change it in the browser settings. Read more on MDN's Intersection Observer page.

Practice exercise

Build a "Back to top" button that only appears after the user scrolls past the page header. Put an Intersection Observer on the <header>: when the header is NOT intersecting, show the button; when it is, hide it. Bonus: add a "Copy page link" button that copies location.href.

Hint: toggle a class with button.classList.toggle('show', !entry.isIntersecting).

Key takeaways

  • IntersectionObserver tells you when elements enter the viewport, without slow scroll listeners.
  • Use it for reveal-on-scroll and custom lazy loading; plain images can use loading="lazy".
  • ResizeObserver reacts to an element's size changes.
  • navigator.clipboard.writeText() copies text on secure pages, inside a user action.
  • Geolocation and notifications need permission: ask at the right moment and always handle "no".

Open this lesson on its own page ↗

Module 16 of 16

Advanced: Professional JavaScript

Lesson 16.1 6 min read

Debugging JavaScript with Browser DevTools

Debugging JavaScript with browser DevTools means finding and fixing bugs using the console, breakpoints, stepping, watch expressions and the Network tab. Learn the common error messages too.

Opening DevTools and using console methods

Every modern browser has DevTools (developer tools). Press F12, or Ctrl+Shift+I (Cmd+Option+I on a Mac). The examples here use Chrome and Edge, but Firefox and Safari have the same main features.

You already know console.log(). The console has more useful methods.

const users = [
  { name: 'Ana', age: 28, role: 'admin' },
  { name: 'Ben', age: 34, role: 'editor' },
];

console.table(users);                 // shows a neat table with columns
console.warn('Low stock: 2 items');   // yellow warning
console.error('Payment failed');      // red error with a stack trace

console.group('Checkout');            // start a collapsible group
console.log('Step 1: cart');
console.log('Step 2: address');
console.groupEnd();

console.time('loop');
for (let i = 0; i < 100_000; i++) {}
console.timeEnd('loop');              // Output: loop: 1.2 ms (your time will differ)

console.count('clicked');             // Output: clicked: 1
console.count('clicked');             // Output: clicked: 2

console.assert(users.length > 5, 'Expected more than 5 users');
// Output: Assertion failed: Expected more than 5 users

console.table() is great for arrays of objects. console.group() keeps related logs together. console.assert() only prints when the condition is false.

Tip: Log objects with a label, like console.log('user:', user) or console.log({ user }). The second form shows the variable name too, so you know which value you are looking at.

How to use breakpoints and the debugger statement

console.log is fine, but a breakpoint is more powerful. It pauses your code at a line, like pressing pause on a video, so you can look at every variable at that moment.

To set one: open the Sources panel (called Debugger in Firefox), find your file, and click the line number. A blue marker appears. Run the code, and it stops there.

You can also pause from inside your code with the debugger statement.

<button id="addBtn">Add to cart</button>
<p id="total">Total: $0</p>

<script>
  const cart = [];

  function calculateTotal(items) {
    let total = 0;
    for (const item of items) {
      debugger;                     // DevTools pauses here when open
      total += item.price * item.qty;
    }
    return total;
  }

  document.querySelector('#addBtn').addEventListener('click', () => {
    cart.push({ name: 'Book', price: '12', qty: 2 });  // bug: price is a string
    document.querySelector('#total').textContent = `Total: $${calculateTotal(cart)}`;
  });
</script>

Open DevTools, then click the button. The code pauses on debugger. Hover over item and you see price: '12' is a string. Here it still works because * converts strings to numbers, but if you later change it to total += item.price, you would get "012". Breakpoints help you see these things.

Common mistake: Forgetting debugger in your code. It does nothing for normal users, but remove it before you publish, as it will pause the page for anyone who has DevTools open.

Other breakpoint types

  • Conditional breakpoint: right-click a line number and pick "Add conditional breakpoint", for example item.qty > 10. It only pauses when the condition is true.
  • Logpoint: right-click and pick "Add logpoint" to print a message without editing your code.
  • Event listener breakpoints: in the Sources panel, pause on any click or submit, even if you don't know where the handler is.

Stepping through code and watch expressions

When the code is paused, the toolbar has step buttons:

ButtonWhat it does
Resume (F8)Continue running until the next breakpoint
Step over (F10)Run the current line, then stop on the next one
Step into (F11)Go inside the function called on this line
Step out (Shift+F11)Finish the current function and stop where it was called

On the right side you will see Scope (all variables available right now) and Call Stack (the chain of functions that led here). In the Watch section, click + and type any expression, like total or item.price * item.qty. Its value updates every time you step. This is how you watch a bug happen line by line.

Using the Network tab to fix fetch problems

When fetch() does not work, open the Network tab and reload the page. Click the request to see its details.

  • Status: 200 is OK, 404 means wrong URL, 401 or 403 means not allowed, 500 means the server crashed.
  • Headers: check the exact URL and method that was sent.
  • Payload: the data you sent with POST.
  • Response or Preview: what the server actually returned.
async function loadUser() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users/9999');
    if (!response.ok) {
      throw new Error(`Server answered ${response.status}`);
    }
    const user = await response.json();
    console.log(user.name);
  } catch (error) {
    console.error('Could not load user:', error.message);
  }
}
loadUser();
// Output: Could not load user: Server answered 404

In the Network tab you will see this request in red with status 404. Remember: fetch() does not throw for a 404 or 500 by itself, so always check response.ok.

Common JavaScript error messages explained

Error messageWhat it usually meansHow to fix it
TypeError: x is not a functionYou called something that isn't a function, often a typo or wrong method nameCheck spelling, e.g. addEventListner, or log typeof x
TypeError: Cannot read properties of null (reading 'addEventListener')querySelector found nothingCheck the selector, or load the script with defer so the HTML exists
TypeError: Cannot read properties of undefined (reading 'name')The object before .name is missingLog the object; data may not have loaded yet; consider ?.
ReferenceError: total is not definedThe variable does not exist hereCheck spelling and scope (was it declared inside another block?)
SyntaxError: Unexpected tokenA typo in the code: missing bracket, comma or quoteClick the file link to jump to the line, and look just before it
Uncaught (in promise)An async error that nobody caughtAdd try...catch around your await code
Failed to fetch or a CORS errorNetwork problem, or the server does not allow your siteCheck the URL in the Network tab; CORS must be fixed on the server

The file name and line number next to each error is a link. Click it to jump straight to the problem.

Practice exercise

Take this buggy code: const prices = [10, 20, 30]; let sum; for (let i = 0; i <= prices.length; i++) { sum += prices[i]; } console.log(sum);. It prints NaN. Put a breakpoint inside the loop, add sum and prices[i] as watch expressions, and step until you find both bugs.

Hint: look at the starting value of sum and at what prices[i] is on the last loop.

Key takeaways

  • Use console.table, console.group, console.time and console.assert for clearer logs.
  • Breakpoints and the debugger statement pause code so you can inspect every variable.
  • Step over, into and out, and use watch expressions to follow a bug line by line.
  • The Network tab shows the status, request and response of every fetch call.
  • Most errors have a common cause: read the message, click the line link, and check what is null or undefined.

Open this lesson on its own page ↗

Code copied to clipboard!