The Language of Behaviour

JavaScript: Making Pages Think

HTML is structure, CSS is style — JavaScript is logic. It's a real programming language that lets your page make decisions, react, calculate and change on the fly. This is where you become a programmer.

18 modules ~90 min read Animated examples Core programming logic
Module 1

Variables: Boxes That Hold Data

A program is just data being moved and transformed. A variable is a named box in the computer's memory where you store a value to use later.

const and let

Modern JavaScript gives you two keywords. Use const when the value will never be reassigned (your default choice), and let when it will change over time. Avoid the old var.

script.js
const name = "Sara";   // text — never changes
let score = 0;        // a number that will go up
score = score + 10;    // now score holds 10
Animated example

Values being stored in named boxes

const name"Sara"
let score0
let isWinnerfalse
  1. Store the text "Sara" in a box called name.
  2. Create a score box holding the number 0.
  3. Reassign it: score = score + 10. The box now holds 10 — that's why it's a let.
  4. A boolean isWinner starts false.
  5. Later the game flips it to true. Variables let the program remember state.
Module 2

Data Types & Operators

Every value has a type. Knowing the type tells you what you can do with it.

The types you'll use daily

  • String — text in quotes: "hello", 'world'.
  • Number42, 3.14. No separate integer/decimal type.
  • Booleantrue or false. The heart of all logic.
  • Array — an ordered list: [1, 2, 3].
  • Object — labelled data: { name: "Sara" }.
  • null / undefined — "nothing here" and "not set yet".

Operators do the work

Arithmetic: + - * / % (the % remainder is surprisingly useful). Comparison: === !== > < >= <=. Logical: && (and), || (or), ! (not).

Animated example

Evaluating expressions in the console

> 2 + 3 * 4
14 // * runs before +
> "Ria" + " " + "Academy"
"Ria Academy" // + joins strings
> 10 === "10"
false // number ≠ string
> age >= 18 && hasTicket
true // both sides true
  1. Maths follows order of operations: * before +, giving 14.
  2. The + operator also concatenates (joins) strings.
  3. === is strict equality — it checks value and type. A number is never equal to a string.
  4. && is true only if both sides are true — the building block of real decisions.

Always use ===, never ==The loose == does surprising type conversions (0 == "" is true!). The strict === is predictable. Professionals use === by default.

Module 3

Making Decisions: if / else

This is where code stops being a fixed script and starts thinking. An if statement runs a block of code only when a condition is true.

script.js
if (age >= 18) {
  console.log("Access granted");
} else {
  console.log("Access denied");
}
Animated example

The same condition, two different inputs

age = ?
✔ Access granted
✘ Access denied
  1. First run: the user's age is 20.
  2. 20 >= 18 is true, so the if block runs — access granted.
  3. Second run: age is now 15.
  4. 15 >= 18 is false, so the else block runs instead — access denied.

Chain more conditions with else if, and for many fixed cases a switch statement reads more cleanly. But if / else if / else covers the vast majority of real code.

Module 4

Loops: Repeating Without Copy-Paste

Computers are brilliant at doing the same thing thousands of times. A loop repeats a block of code until a condition says stop.

script.js
for (let i = 1; i <= 5; i++) {
  console.log("Count: " + i);
}

The for loop has three parts: a start (let i = 1), a condition that keeps it going (i <= 5), and a step that runs each time (i++, meaning "add 1 to i").

Animated example

A for-loop counting 1 to 5

1
2
3
4
5
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
  1. i starts at 1. Condition 1 <= 5 is true → run the body, print "Count: 1".
  2. i++ makes it 2. Still ≤ 5 → run again.
  3. 3 — the loop keeps going, printing each time.
  4. 4 — one more pass.
  5. 5 — the last valid value. Next i is 6, 6 <= 5 is false, so the loop stops.

Other loopswhile repeats as long as a condition holds (when you don't know the count in advance). for...of loops straight over an array's items — you'll meet it in the next module.

Module 5

Functions: Reusable Machines

A function is a named block of code you can run whenever you want. You build the machine once, then "call" it as many times as you like — with different inputs, giving different outputs.

script.js
function add(a, b) {
  return a + b;
}
add(5, 10);   // → 15

// modern "arrow function" — same thing, shorter
const add = (a, b) => a + b;

The values in the brackets (a, b) are parameters — placeholders for the inputs. return hands a value back to whoever called the function.

Animated example

Input → function → output

5
10
function
add(a, b)
return a + b
?
  1. We call the function with two inputs: add(5, 10).
  2. Inside, the parameters become a = 5 and b = 10. The machine runs a + b.
  3. It returns 15 — the output.
  4. Call it again with different inputs and you get a different result. Write once, reuse forever.

Functions are how big programs stay saneInstead of one giant wall of code, you split work into small, named, testable machines. "Don't Repeat Yourself" (DRY) is the golden rule — if you're copy-pasting code, it probably wants to be a function.

Module 6

Arrays & Objects

Real apps handle lists of things and bundles of related data. Arrays and objects are the two structures that make that possible — you'll use them in every program you ever write.

Arrays: ordered lists

An array holds many values in order. Each item has an index, and — this catches everyone — counting starts at 0, not 1.

Animated example

An array and its indexes

[0]"🍎"
[1]"🍌"
[2]"🍇"
> fruits[1]
"🍌" // index 1 = 2nd item
> fruits.length
3
  1. const fruits = ["🍎", "🍌", "🍇"] — the first item sits at index 0.
  2. The banana is at index 1
  3. …and the grapes at index 2.
  4. So fruits[1] returns "🍌" — access items by their index.
  5. fruits.length tells you how many items there are: 3.

Objects: labelled data

An object groups related values under named keys instead of numbered indexes — perfect for describing one "thing":

script.js
const user = {
  name: "Sara",
  age: 25,
  isAdmin: true
};
user.name;   // → "Sara"  (access with a dot)

Arrays and objects combineReal data is usually an array of objects — e.g. a list of users, each a { name, age } object. Master these two and you can model almost any information an app needs.

Module 7

Array Methods That Power Real Apps

Once you have a list of data, you rarely want the whole list untouched — you want to transform it, filter it, or display each item. Three methods do 90% of this work, and modern JavaScript is built on them.

Each one takes a function (from Module 5) and runs it on every item. This is where functions and arrays combine into real power:

  • forEach()do something with each item (e.g. print it). Returns nothing.
  • map()transform each item into a new array of the same length.
  • filter()keep only items that pass a test, into a new, smaller array.
script.js
const nums = [1, 2, 3, 4];

const doubled = nums.map(n => n * 2);    // [2, 4, 6, 8]
const big = doubled.filter(n => n > 4); // [6, 8]
Animated example

map transforms, filter selects

nums
1
2
3
4
.map(n => n * 2) ↓
doubled
2
4
6
8
.filter(n => n > 4) ↓
big
6
8
  1. map runs n * 2 on every item, producing a new array [2, 4, 6, 8] — same length, transformed values.
  2. filter keeps only items where n > 4, producing a smaller array [6, 8]. The originals are never changed.

This is how real UIs are builtA feed of posts? posts.map(post => renderCard(post)). A search box? items.filter(i => i.name.includes(query)). You'll see .map() everywhere in the React course — it's the standard way to turn a data array into a list of elements on screen.

Module 8

Scope, Hoisting & Closures

Where do your variables actually live, and who can see them? Getting this right is the difference between code that behaves and code full of mysterious bugs — and it leads to closures, one of JavaScript's most powerful ideas (and its most-asked interview question).

Scope: where a variable can be seen

Scope is the region of your code where a variable exists. There are three levels, from widest to narrowest:

  • Global scope — declared outside any function; visible everywhere. Use sparingly.
  • Function scope — declared inside a function; visible only inside that function.
  • Block scopelet and const declared inside any { } block (an if, a loop) exist only inside those braces.

The golden rule: inner scopes can see outward, but outer scopes cannot see inward. A function can read the variables around it, but the outside world cannot reach in and read the function's private variables.

script.js
const appName = "Ria";   // global — visible anywhere

function greet() {
  const message = "Hello";  // function-scoped
  console.log(message, appName);  // ✓ sees both
}
console.log(message);  // ✖ Error: message is not defined out here

Hoisting, brieflyJavaScript "hoists" declarations to the top of their scope before running. With var this causes confusing bugs (a variable exists as undefined before its line). let and const are hoisted too but sit in a "temporal dead zone" — using one before its declaration throws a clear error instead. This is one more reason professionals avoid var.

Closures: a function that remembers

Here is the magic. A closure is a function that keeps access to the variables of the scope it was born in — even after that outer scope has finished running. The inner function carries its birthplace around with it, like a backpack.

counter.js
function makeCounter() {
  let count = 0;              // private to this call
  return function () {
    count++;                    // remembers count forever
    return count;
  };
}
const next = makeCounter();
next(); // 1   next(); // 2   next(); // 3
Animated example

A closure remembering its private count

makeCounter() scope
let count = 0
?
  1. makeCounter() runs once and creates a private count = 0 inside its own scope.
  2. It returns the inner function. Even though makeCounter has now finished, the returned function still holds a live link to count — that link is the closure.
  3. Call next()count becomes 1. The variable survived.
  4. Call it again → 2, again → 3. Nothing outside can touch count; only this function can. Private, protected, persistent state.

Why closures matterThey give you private data in a language that has no real "private" keyword, and they are the foundation of countless patterns you'll meet — React's useState, event handlers that remember values, and function factories all rely on closures. If a callback "remembers" a variable from where it was written, that's a closure at work.

Module 9

Asynchronous JavaScript

JavaScript does one thing at a time. So what happens when a task takes time — fetching data from a server, waiting for a timer? If JS just froze until it finished, every page would lock up. This module is how JavaScript stays responsive: it's the concept that trips up almost every beginner, and mastering it is what makes you dangerous.

Synchronous vs asynchronous

Synchronous code runs line by line, each waiting for the previous to finish — instant for normal work. But some tasks are asynchronous: they start now and finish later (a network request, a 3-second timer). JavaScript does not wait for them — it kicks them off, keeps running the rest of your code, and deals with the result whenever it arrives. This "don't block, react later" model is the whole game.

The coffee-shop modelYou order a coffee (start an async task) and get a buzzer. Instead of standing frozen at the counter blocking everyone, you sit down and carry on (other code runs). When the buzzer goes off (the data arrives), you go collect it. JavaScript works exactly like this.

Promises & async/await

An async task hands you back a Promise — an IOU for a value that will arrive later. The modern way to work with promises is async/await, which lets you write asynchronous code that reads top-to-bottom like normal code:

script.js
async function loadUser() {
  console.log("1: Start");
  const res = await fetch("/api/user"); // pause, don't freeze
  const data = await res.json();
  console.log("Data:", data);
}
Animated example

Why async code doesn't run in the order you'd expect

console.log("A") fetch(...) // starts, takes time console.log("B") ... data arrives ...
A ← prints instantly
fetching… (not blocking)
B ← prints BEFORE the data!
Data loaded ✓ ← arrives last
  1. The first line runs and prints A instantly — normal synchronous code.
  2. fetch() fires. It takes time, so JavaScript does not wait — it starts the request and moves on. A spinner shows it's pending in the background.
  3. The very next line runs immediately and prints Bbefore the data has arrived. This surprises every beginner: the output order is A, B, then the data.
  4. Seconds later the response finally arrives, and the code after await resumes, printing the data last. The page never froze while waiting.

Always handle failureNetworks fail. Wrap await calls in try { … } catch (err) { … } so a dropped connection shows a friendly message instead of a broken page. Real async code always plans for the request that doesn't come back.

Module 10

Modern JavaScript (ES6+)

Everything so far is valid, timeless JavaScript. But since 2015 the language gained a set of features — collectively "ES6+" — that make code dramatically shorter and clearer. You'll see them in every codebase and every React tutorial, so let's make them familiar.

Template literals

Instead of clumsy + string joining, wrap text in backticks and drop variables in with ${...}:

script.js
// old way
const msg = "Hi " + name + ", you have " + n + " messages";
// template literal — much cleaner
const msg = `Hi ${name}, you have ${n} messages`;

Destructuring & spread

Destructuring pulls values straight out of an object or array into named variables. The spread operator (...) copies or merges them. These two show up constantly, especially in React:

script.js
const user = { name: "Sara", age: 25 };
const { name, age } = user;   // name = "Sara", age = 25

const more = { ...user, city: "Cairo" }; // copy + add
Animated example

Destructuring pulls values into variables

const user{ name:"Sara", age:25 }
const { … } = user
name"Sara"
age25
  1. We have an object user with a name and an age.
  2. Destructuring names the pieces we want inside { } on the left.
  3. JavaScript pulls each matching value out into its own variable — name and age now hold the values directly. One line instead of two.

Also worth knowingArrow functions (const add = (a, b) => a + b) are the short function form you'll use everywhere. Default parameters (function greet(name = "friend")) and ES modules (import / export to split code across files) round out the modern toolkit that React and every build tool rely on.

Module 11

Error Handling & Debugging

Code breaks — for everyone, constantly. The difference between a beginner and a professional isn't writing code that never fails; it's writing code that fails gracefully and knowing how to hunt down the cause. This module is both skills.

try / catch / finally

Some operations can throw an error and crash everything after them — bad JSON, a failed network call, unexpected input. Wrap the risky part in try, handle any failure in catch, and put cleanup that must always run in finally:

script.js
try {
  const data = JSON.parse(badInput);  // might throw
  show(data);
} catch (err) {
  console.error(err.message);       // runs only on failure
  showMessage("Something went wrong");
} finally {
  hideSpinner();                     // always runs
}
Animated example

A crash caught and handled

JSON.parse(badInput)
catch → "Something went wrong" ✓
finally → hide spinner (always)
  1. A risky line — parsing malformed input — is about to run.
  2. We put it inside try, so JavaScript watches it.
  3. It throws a SyntaxError — unhandled, this would crash the whole page.
  4. But catch intercepts it and shows a friendly message. The app survives.
  5. finally runs no matter what — success or failure — perfect for cleanup like hiding a loading spinner.

Debugging: reading the error, not fearing it

When something breaks, the browser's console (F12 → Console) is your best friend. An error message tells you what went wrong, on which line, and the "stack trace" shows the chain of calls that led there. Add console.log(value) to inspect what a variable actually holds, or set a breakpoint in the Sources tab to pause execution and step through line by line.

Read errors slowlyThe instinct is to panic and change things randomly. Resist it. Read the message, go to the line number, and check your assumptions with a console.log. Nine times out of ten a variable is undefined or a name is misspelled. Calm, methodical debugging is a superpower.

Module 12

Object-Oriented JavaScript (OOP)

As your applications grow, you need better ways to organize code. Object-Oriented Programming (OOP) groups data and the functions that operate on that data into single units called Classes.

Classes and instances

A class is like a blueprint. An instance is the actual object built from that blueprint. The constructor function runs automatically when you create a new instance to set up its initial state.

script.js
class User {
  constructor(name, level) {
    this.name = name;
    this.level = level;
  }

  levelUp() {
    this.level++;
    console.log(`${this.name} is now level ${this.level}!`);
  }
}

const player1 = new User("Ali", 1);
player1.levelUp();   // "Ali is now level 2!"
Animated example

Instantiating a class to create an object

class User {
constructor(name, level)
levelUp() }
player1 {}
player1.levelUp()
  1. The User class is our blueprint. It defines what data and methods users have.
  2. We call new User("Ali", 1). The constructor runs and creates a fresh object for player1.
  3. The object is fully formed: { name: "Ali", level: 1 }.
  4. We call player1.levelUp(). The method modifies only this specific instance, updating its level to 2.

Inheritance

Classes can also extend other classes. This allows you to create specialized versions of a blueprint without rewriting code:

script.js
class Admin extends User {
  constructor(name, level, permissions) {
    super(name, level); // calls the User constructor
    this.permissions = permissions;
  }
}

The magic of "this"Inside a class, this refers to the specific instance that is currently executing the code. It allows player1 and player2 to share the same levelUp method but update their own separate level numbers.

Module 13

Data & Web Storage

By default, JavaScript forgets everything the moment the user refreshes the page. To build real applications, you need to remember things. The browser provides localStorage to save data permanently on the user's device.

Saving and Loading

LocalStorage only stores strings. If you want to store an array or an object, you must convert it to a string using JSON.stringify(), and convert it back when loading with JSON.parse().

script.js
const settings = { theme: "dark", notifications: true };

// Save: Convert object to string
localStorage.setItem("userSettings", JSON.stringify(settings));

// Load: Retrieve string and convert back to object
const saved = localStorage.getItem("userSettings");
if (saved) {
  const parsed = JSON.parse(saved);
  console.log(parsed.theme); // "dark"
}
Animated example

Bridging the gap between code and storage

Object { theme: "dark" }
JSON.stringify() ↓
JSON.parse() ↑
String (Storage) '{"theme":"dark"}'
  1. JavaScript uses live objects in memory. But storage can only hold plain text strings.
  2. When saving, JSON.stringify() flattens the object into a text string format. This string is safely written to LocalStorage.
  3. When the page reloads, the text string is read back from LocalStorage.
  4. Finally, JSON.parse() converts that text string back into a live JavaScript object ready for use.

Storage Limits and SecurityNever store sensitive information (like passwords or payment tokens) in LocalStorage, as any JavaScript on the page can read it. It is perfect for preferences, shopping carts, or unsaved draft forms. sessionStorage works exactly the same way but clears itself when the browser tab is closed.

Module 14

The "this" Keyword & Context

In JavaScript, this is dynamic. It doesn't refer to the function itself, but to how the function was called. It's one of the most misunderstood concepts, but critical for mastering advanced JS.

Four Rules of "this"

To know what this points to, look at the call site (where the function is executed):

  • Implicit Binding: If there is a dot (e.g., user.greet()), this is the object before the dot.
  • Explicit Binding: Using .call(), .apply(), or .bind() forces this to be exactly what you specify.
  • New Binding: Using the new keyword creates a fresh object and binds this to it.
  • Default Binding: A standalone function call (greet()) binds this to the global object (or undefined in strict mode).
script.js
const user = {
  name: "Sara",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  }
};
user.greet(); // "Hi, I'm Sara" (Implicit binding)

const standaloneGreet = user.greet;
standaloneGreet(); // "Hi, I'm undefined" (Lost context!)

// Fix it with bind
const boundGreet = standaloneGreet.bind(user);
boundGreet(); // "Hi, I'm Sara"

Arrow Functions are SpecialUnlike regular functions, arrow functions do not have their own this. They inherit this from the surrounding lexical scope (where they were written). This makes them perfect for callbacks inside class methods, where you want this to remain the class instance.

Practical Example: Losing Context in Callbacks

One of the most common bugs in JavaScript occurs when passing a method as a callback (e.g., to setTimeout). The method loses its object context and defaults to the global object.

script.js
const player = {
  name: "Mario",
  jump() {
    console.log(`${this.name} jumps!`);
  },
  delayedJump() {
    // Bug: setTimeout calls this as a standalone function
    setTimeout(this.jump, 1000); 
    // Fix 1: setTimeout(this.jump.bind(this), 1000);
    // Fix 2 (Modern): setTimeout(() => this.jump(), 1000);
  }
};
player.delayedJump(); // Output: "undefined jumps!"
Animated example

Context is lost and found

user { name: "Sara" }
> user.greet()
"Hi, I'm Sara" ← this = user
> const fn = user.greet; fn()
"Hi, I'm undefined" ← this = window
> const bound = fn.bind(user); bound()
"Hi, I'm Sara" ← this forced to user
  1. Calling user.greet() directly means the object before the dot (user) becomes this.
  2. Assigning the method to a bare variable fn and calling it without a dot loses the context. this defaults to the global window, which has no name.
  3. Using .bind(user) creates a new function where this is permanently locked to user, fixing the issue.
Module 15

Prototypes & Inheritance

Before classes existed in JS (Module 12), developers used Prototypal Inheritance. Even today, the class keyword is just syntactic sugar over this powerful, underlying prototype system.

The Prototype Chain

Every object in JavaScript has a hidden internal property called [[Prototype]] (accessible via __proto__). When you try to access a property or method on an object, if JS doesn't find it directly on that object, it looks up the prototype chain to see if the prototype has it.

script.js
const animal = {
  eat() {
    console.log("Eating...");
  }
};

const dog = {
  bark: "Woof!",
  __proto__: animal  // dog inherits from animal
};

console.log(dog.bark); // "Woof!" (found on dog)
dog.eat();             // "Eating..." (found on animal via prototype)

This is why array methods like .map() work! All arrays inherit from Array.prototype, where those methods are actually stored. They don't copy the methods onto every single array; they share them efficiently.

Modern approachWhile it's crucial to understand prototypes for debugging and answering interview questions, for writing new code, you should stick to the modern class syntax introduced in ES6. It makes inheritance much easier to read and manage.

Under the Hood: How Classes Use Prototypes

When you define a method inside a class, JavaScript doesn't copy that method to every single instance. Instead, it places the method on the class's prototype object.

script.js
class Car {
  constructor(brand) { this.brand = brand; }
  drive() { console.log("Vroom!"); }
}

const myCar = new Car("Toyota");
console.log(myCar.hasOwnProperty("brand")); // true
console.log(myCar.hasOwnProperty("drive")); // false! It's on the prototype.
console.log(myCar.__proto__ === Car.prototype); // true
Animated example

The Prototype Chain

animal (Prototype) eat() { ... }
dog (Instance) bark: "Woof!"
> dog.bark → found on dog
> dog.eat() → not on dog
Searching __proto__...
Found on animal! runs eat()
  1. Accessing dog.bark is easy: it exists directly on the dog object.
  2. Calling dog.eat() triggers a search. JS looks at dog and doesn't find eat.
  3. Instead of giving up, JS follows the hidden __proto__ link up the chain.
  4. It finds eat() on the animal prototype and runs it successfully. This is how inheritance works!
Module 16

ES Modules (Import/Export)

Writing a real-world application in a single file quickly becomes unmanageable. Modern JavaScript provides Modules, allowing you to split your code into separate files and connect them securely.

Exporting and Importing

You can export variables, functions, or classes from one file and import them into another. This keeps variables scoped to their module, avoiding global scope pollution.

utils.js
// Named exports
export const PI = 3.14159;
export function calculateArea(radius) {
  return PI * radius * radius;
}

// Default export (only one per file)
export default function logMath() {
  console.log("Math module loaded!");
}
main.js
// Import default (no braces) and named (with braces)
import logMath, { PI, calculateArea } from './utils.js';

logMath();
console.log(calculateArea(5)); // 78.53975
Animated example

Connecting files securely

utils.js
export const PI = 3.14
export function calc()

import
main.js
import { calc }
calc() // works!
  1. In utils.js, we explicitly mark calc() with the export keyword.
  2. In main.js, we use import { calc } to bring that specific function into our file.
  3. We can now use calc() as if it were defined in main.js. The files are securely connected without polluting global variables.

Modules in the browserTo use imports in an HTML file natively, you must add type="module" to your script tag: <script type="module" src="main.js"></script>.

Module 17

Advanced Async: Promise.all

Sometimes you need to fetch data from multiple APIs at the same time. Doing this sequentially (one after another) is slow. Promise.all lets you run them in parallel and wait for all of them to finish.

Parallel Execution

If you pass an array of promises to Promise.all, it returns a single promise that resolves with an array of the results in the exact same order.

script.js
async function loadDashboard() {
  console.time("Fetch Time");
  
  // Fire all requests simultaneously!
  const [usersRes, postsRes, statsRes] = await Promise.all([
    fetch("/api/users"),
    fetch("/api/posts"),
    fetch("/api/stats")
  ]);

  // Parse JSON for all of them
  const users = await usersRes.json();
  const posts = await postsRes.json();
  const stats = await statsRes.json();
  
  console.timeEnd("Fetch Time"); // Much faster than waiting for each separately
  renderDashboard(users, posts, stats);
}
Animated example

Parallel vs Sequential Execution

Promise.all([ ... ])
/users (2s)
/posts (1.5s)
/stats (2.5s)
All done in 2.5s!
  1. We trigger three network requests inside Promise.all. They all start at the same time and run in parallel.
  2. The total wait time is only as long as the slowest request (2.5 seconds), not the sum of all three (6 seconds). Huge performance win!

All or nothingIf even one promise in Promise.all fails (rejects), the whole thing fails immediately. If you want to allow some to fail while others succeed, use Promise.allSettled() instead.

Module 18

Regular Expressions (RegEx)

When you need to validate an email, find phone numbers in text, or replace complex string patterns, string methods like .includes() aren't enough. Regular Expressions are a mini-language for pattern matching.

Defining and Testing Patterns

RegEx in JavaScript is written between forward slashes, like /pattern/. You can test if a string matches the pattern using the .test() method.

script.js
// A pattern for a valid email (basic)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

console.log(emailRegex.test("hello@world.com")); // true
console.log(emailRegex.test("hello world"));     // false

// Using RegEx for advanced string replacement
const text = "I love apples. Apples are great.";
// /apples/gi means: Global (all matches) and Case-Insensitive
const newText = text.replace(/apples/gi, "bananas"); 
console.log(newText); // "I love bananas. bananas are great."
Animated example

Pattern matching with RegEx

/apples/gi
"I love apples. Apples are great."
replace("bananas")
"I love bananas. bananas are great."
  1. We have a string of text and a regex pattern: find "apples", globally (g), case-insensitive (i).
  2. It finds the first match: "apples".
  3. Thanks to the i flag, it also matches "Apples" with a capital A. Thanks to g, it didn't stop after the first match.
  4. We call .replace() with the replacement text "bananas".
  5. Every match is cleanly replaced in one go.

You're programming now 🧠

Variables, types, conditionals, loops, functions, arrays and objects — this is the core of every programming language, not just JavaScript. Next you'll use it to make real web pages react to the user.