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.
const name = "Sara"; // text — never changes
let score = 0; // a number that will go up
score = score + 10; // now score holds 10Values being stored in named boxes
- Store the text
"Sara"in a box calledname. - Create a
scorebox holding the number0. - Reassign it:
score = score + 10. The box now holds10— that's why it's alet. - A boolean
isWinnerstartsfalse. - Later the game flips it to
true. Variables let the program remember state.
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'. - Number —
42,3.14. No separate integer/decimal type. - Boolean —
trueorfalse. 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).
Evaluating expressions in the console
- Maths follows order of operations:
*before+, giving14. - The
+operator also concatenates (joins) strings. ===is strict equality — it checks value and type. A number is never equal to a string.&&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.
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.
if (age >= 18) {
console.log("Access granted");
} else {
console.log("Access denied");
}The same condition, two different inputs
- First run: the user's
ageis20. 20 >= 18is true, so theifblock runs — access granted.- Second run:
ageis now15. 15 >= 18is false, so theelseblock 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.
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.
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").
A for-loop counting 1 to 5
istarts at 1. Condition1 <= 5is true → run the body, print "Count: 1".i++makes it 2. Still ≤ 5 → run again.- 3 — the loop keeps going, printing each time.
- 4 — one more pass.
- 5 — the last valid value. Next
iis 6,6 <= 5is 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.
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.
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.
Input → function → output
add(a, b)
return a + b
- We call the function with two inputs:
add(5, 10). - Inside, the parameters become
a = 5andb = 10. The machine runsa + b. - It
returns 15 — the output. - 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.
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.
An array and its indexes
const fruits = ["🍎", "🍌", "🍇"]— the first item sits at index 0.- The banana is at index 1…
- …and the grapes at index 2.
- So
fruits[1]returns "🍌" — access items by their index. fruits.lengthtells 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":
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.
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.
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]map transforms, filter selects
maprunsn * 2on every item, producing a new array[2, 4, 6, 8]— same length, transformed values.filterkeeps only items wheren > 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.
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 scope —
letandconstdeclared inside any{ }block (anif, 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.
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 hereHoisting, 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.
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(); // 3A closure remembering its private count
makeCounter()runs once and creates a privatecount = 0inside its own scope.- It returns the inner function. Even though
makeCounterhas now finished, the returned function still holds a live link tocount— that link is the closure. - Call
next()→countbecomes 1. The variable survived. - 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.
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:
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);
}Why async code doesn't run in the order you'd expect
- The first line runs and prints A instantly — normal synchronous code.
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.- The very next line runs immediately and prints B — before the data has arrived. This surprises every beginner: the output order is A, B, then the data.
- Seconds later the response finally arrives, and the code after
awaitresumes, 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.
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 ${...}:
// 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:
const user = { name: "Sara", age: 25 };
const { name, age } = user; // name = "Sara", age = 25
const more = { ...user, city: "Cairo" }; // copy + addDestructuring pulls values into variables
- We have an object
userwith anameand anage. - Destructuring names the pieces we want inside
{ }on the left. - JavaScript pulls each matching value out into its own variable —
nameandagenow 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.
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:
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
}A crash caught and handled
- A risky line — parsing malformed input — is about to run.
- We put it inside
try, so JavaScript watches it. - It throws a
SyntaxError— unhandled, this would crash the whole page. - But
catchintercepts it and shows a friendly message. The app survives. finallyruns 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.
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.
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!"Instantiating a class to create an object
constructor(name, level)
levelUp() }
- The
Userclass is our blueprint. It defines what data and methods users have. - We call
new User("Ali", 1). Theconstructorruns and creates a fresh object forplayer1. - The object is fully formed:
{ name: "Ali", level: 1 }. - 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:
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.
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().
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"
}Bridging the gap between code and storage
- JavaScript uses live objects in memory. But storage can only hold plain text strings.
- When saving,
JSON.stringify()flattens the object into a text string format. This string is safely written to LocalStorage. - When the page reloads, the text string is read back from LocalStorage.
- 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.
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()),thisis the object before the dot. - Explicit Binding: Using
.call(),.apply(), or.bind()forcesthisto be exactly what you specify. - New Binding: Using the
newkeyword creates a fresh object and bindsthisto it. - Default Binding: A standalone function call (
greet()) bindsthisto the global object (orundefinedin strict mode).
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.
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!"Context is lost and found
- Calling
user.greet()directly means the object before the dot (user) becomesthis. - Assigning the method to a bare variable
fnand calling it without a dot loses the context.thisdefaults to the global window, which has noname. - Using
.bind(user)creates a new function wherethisis permanently locked touser, fixing the issue.
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.
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.
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); // trueThe Prototype Chain
- Accessing
dog.barkis easy: it exists directly on thedogobject. - Calling
dog.eat()triggers a search. JS looks atdogand doesn't findeat. - Instead of giving up, JS follows the hidden
__proto__link up the chain. - It finds
eat()on theanimalprototype and runs it successfully. This is how inheritance works!
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.
// 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!");
}// Import default (no braces) and named (with braces)
import logMath, { PI, calculateArea } from './utils.js';
logMath();
console.log(calculateArea(5)); // 78.53975Connecting files securely
export function calc()
import
calc() // works!
- In
utils.js, we explicitly markcalc()with theexportkeyword. - In
main.js, we useimport { calc }to bring that specific function into our file. - We can now use
calc()as if it were defined inmain.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>.
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.
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);
}Parallel vs Sequential Execution
- We trigger three network requests inside
Promise.all. They all start at the same time and run in parallel. - 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.
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.
// 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."Pattern matching with RegEx
- We have a string of text and a regex pattern: find "apples", globally (
g), case-insensitive (i). - It finds the first match: "apples".
- Thanks to the
iflag, it also matches "Apples" with a capital A. Thanks tog, it didn't stop after the first match. - We call
.replace()with the replacement text "bananas". - 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.