JavaScript Real World Vademecum
Part I: Fundamentals & Syntax
Welcome to the foundation of JavaScript. Before diving into complex apps, we must master the building blocks: how to store data, how to make decisions, and how to repeat actions efficiently.
Fundamentals and Data Types
1. Variables - Data Containers
Variables are labeled “boxes” where the program stores information. Imagine moving house: you have different boxes for different items. Some you open and close constantly, others you seal because their reference must not change.
But there’s more: each type of box has its own special rules. Some boxes can be moved from one room to another (scope), others stay fixed where you put them. Some can be emptied and refilled with completely different items, others accept changes only to their internal contents.
let – The Reusable Box (or the Whiteboard)
let creates a variable whose value can be changed over time. It’s like a whiteboard in the kitchen where you write the shopping list: you update it, erase it, and rewrite it continuously.
let message = "Hi";
message = "Goodbye"; // Perfect, I can change the value
let counter = 0;
counter++; // Same as counter = counter + 1
But why is it called let? Think of when you say “let this variable be...” — it’s permission you give JavaScript to have a flexible container. It’s like telling the program: “I allow you to manage this value, and I allow you to change it when needed.”
Key Feature: Block Scope
A let variable exists only inside the {...} block where it was born. Think of an electronic key that only works for a specific hotel room: outside that room, it’s useless.
This concept is revolutionary compared to the old var. It’s as if every pair of curly braces created an invisible bubble: what happens in the bubble stays in the bubble. If you try to use that variable outside its bubble, JavaScript will tell you “I don’t know what you’re talking about!”
{
let secret = "I'm in here";
console.log(secret); // Works!
}
// console.log(secret); // ERROR! 'secret' doesn't exist out here
When to use it?
When you already know that the value of that variable will need to change. But it’s not just about “changing” — it’s about intention. You use let when you’re saying: “This thing will evolve during the execution of my program.”
Perfect examples:
- Counters: They must increment on each loop
- Temporary state: Like the current position in a game
- Accumulators: When you’re building something piece by piece
- Control flags: Variables that track conditions that change
const – The Sealed Box (or the Safe)
const creates a variable that cannot be reassigned to a new value or reference. It’s like carving something into marble: once written, the reference stays the same.
const PI = 3.14159;
// PI = 3.14; // ERROR! You can't reassign a constant.
But watch out! There’s an important mental trick here. const does not mean “constant” in the mathematical sense. It means “constant reference”. It’s the difference between saying “this safe can’t be moved” and “the contents of the safe can’t be touched”.
Crucial Concept: Container vs. Content
const locks the container, not necessarily the content. If the const variable contains a complex type like an Object or an Array, you can still modify its internal properties.
const user = { name: "Mario" };
user.name = "Luigi"; // OK! You're modifying the content.
// user = { name: "Carlo" }; // ERROR! You're trying to change the container.
const numbers = [1, 2, 3];
numbers.push(4); // OK! You're modifying the content.
// numbers = [5, 6]; // ERROR! You're trying to change the container.
The analogy of a safe bolted to the floor is perfect: you can’t move the safe (change the reference), but you can open the door and change the objects inside (modify properties). It’s as if const said: “This variable will always point to THIS specific object in memory, but what’s inside the object can change.”
When to use it?
Always, as a first choice. This is an important mindset shift: always start with const and move to let only when you are absolutely sure you will need to reassign the variable.
Why? Because it makes your code more predictable. When you see const, you know that variable will always point to the same thing. It’s a promise you make to whoever will read the code (including future you): “This thing won’t change reference, you can trust it.”
var – The Old Way (To Avoid)
var is how variables were declared before let and const (before ES6). It has less predictable behavior (the function scope instead of block scope) that can lead to hard-to-find bugs.
Imagine var like an old lock that sometimes opens by itself, or like a container that magically appears in places you don’t expect. It has this strange behavior called hoisting.
Hoisting
JavaScript, before executing the code, takes all var declarations and “lifts” (hoists) them to the start of their function (or the global start), initializing them to undefined. It’s as if your code got rearranged without your knowing!
// What you write
function test() {
console.log(x); // Prints 'undefined' (doesn't throw an error!)
var x = 5;
console.log(x); // Prints 5
}
// What JavaScript "sees" and executes
function test() {
var x; // 1. Declaration "hoisted" and initialized to undefined
console.log(x); // 2. Prints 'undefined'
x = 5; // 3. Assignment
console.log(x); // 4. Prints 5
}
Avoid it in modern projects. If you see var in old code, consider refactoring it (replacing it with let or const). It’s like still seeing Windows XP in an office in 2025 — it works, but why risk it?
null and undefined – Intentional vs. Accidental Absence
These two values represent “nothing”, but with profoundly different meanings. It’s a subtle but extremely important distinction that shows the programmer’s intention.
null
It is the intentional absence of a value. You, the programmer, decide to assign null to indicate that “here, deliberately, there is nothing”.
- Deeper analogy: An empty seat at the table, but set. It’s not that you forgot to put the plate — you consciously decided that seat should remain empty for now. Maybe you’re waiting for a guest who might arrive, or maybe you want to signal that someone left. The point is: there was a conscious decision.
let currentSong = null; // "There is no song playing, and I know it"
let selectedUser = null; // "The user hasn't selected anything yet"
undefined
It is the accidental absence or the “not yet defined” state. It’s the default value of a variable that has been declared but hasn’t yet been assigned a value. JavaScript puts it there automatically, as if saying “Uh, I don’t know what to put here.”
- Deeper analogy: It’s like opening a box you just bought and finding it empty — not because it was meant to be empty, but because nobody has put anything in it yet. Or like a form with a field left blank — you don’t know whether it was left blank on purpose or someone forgot to fill it in.
let nextSong;
console.log(nextSong); // undefined - "I have no idea what it is"
const user = { name: "Mario" };
console.log(user.age); // undefined - "This property has not been defined"
The philosophical difference is deep: null is the Buddhist emptiness — an emptiness full of meaning. undefined is existential emptiness — an emptiness that doesn’t even know it’s empty.
2. Data Types - The Shapes of Information
In JavaScript, every piece of data has its own “shape”. Just like in the kitchen you use different containers for liquids, solids, and spices, in programming you use different structures for text, numbers, and collections of data. But each shape has its own rules, its own superpowers, and its own limitations. Understanding these shapes is essential to avoid confusion—like trying to pour flour into a strainer.
Strings (String) - Text ✍️
Strings are sequences of characters. But thinking of them only as “text” is reductive. They’re like LEGO bricks in the programming world: you can combine them, break them apart, transform them, search inside them. They’re the form any information takes when you want to show or communicate something to a user.
Template Literals (``)
Backticks (or grave accents, ``) are the best and most modern choice for creating strings. Their superpower is interpolation: they let you insert variables or JavaScript expressions directly into text using the ${...} syntax.
const name = "Mario";
const age = 25;
// Old way (clunky)
const oldIntro = "My name is " + name + " and I am " + age + " years old.";
// Modern way (clean and readable)
const intro = `My name is ${name} and I am ${age} years old.`;
// You can also run calculations inside ${}
const price = 100;
const message = `The total is €${price * 1.22} (VAT included)`;
But why are they so powerful? Because they turn a string from a monolithic block into something dynamic and alive. It’s like the difference between a photograph (a static string) and a video (a template literal): they can change, adapt, react to data. Also, they natively handle line breaks without needing \n.
Escape Characters - Special Characters
Sometimes you need to insert special characters into text. The backslash \ is your master key: it tells JavaScript “the next character is special—don’t interpret it as a command”.
const shop = "I am in the \"Store\""; // Quotes inside quotes
const lines = "First line\nSecond line"; // \n = New line
const columns = "First name\tLast name\tAge"; // \t = Tab for alignment
const path = "C:\\Users\\Documents"; // \\ = Literal backslash
const apostrophe = 'The\' apostrophe'; // \' = Apostrophe inside single quotes
It’s like making “air quotes” with your fingers while talking: the backslash is the gesture that says “careful, this is literal, not a command!”
Useful Methods (The Toolbox for Text)
Every string in JavaScript is secretly an object with dozens of hidden methods. It’s as if every word you type comes with a complete toolkit to modify it.
const text = "JavaScript is powerful";
// Basic properties and methods
text.length; // 20 - Not a method but a property!
text.toUpperCase(); // "JAVASCRIPT IS POWERFUL"
text.toLowerCase(); // "javascript is powerful"
// Search
text.includes("powerful"); // true - Searches for a substring
text.indexOf("Script"); // 4 - Where it starts (-1 if not found)
// Cleanup and replacement
" spaces everywhere ".trim(); // "spaces everywhere"
text.replace("powerful", "fantastic"); // Replaces the *first* occurrence
text.replaceAll("e", "3"); // Replaces *all* occurrences
The .concat() Method vs Template Literals
What it does: Joins two strings.
const base = "https://site.com/";
const path = "photo.jpg";
// 1. Object-oriented (The Verb) 🐢
// "Hey base, concatenate path to yourself"
const url1 = base.concat(path);
// 2. Mathematical (Intuitive) ➕
const url2 = base + path;
// 3. Modern (The Winner) 🏆
const url3 = `${base}${path}`;
Why does .concat() exist?: It follows the Subject (base) -> Verb (.concat) -> Object (path) logic.
Tip: Learn it for tests, but in real life use Template Literals (option 3). They’re more readable and powerful.
The .split() Method - The String Slicer
.split() is like a magic knife that cuts a string wherever you decide. But the real magic is that it turns a string into an array: it goes from a single block to a list of pieces you can manipulate individually.
"Hello happy world".split(' '); // ['Hello', 'happy', 'world']
"2025-01-15".split('-'); // ['2025', '01', '15']
"hello".split(""); // ["h", "e", "l", "l", "o"] - Every letter!
The separator you choose is like deciding where to cut a cake.
.charCodeAt() vs .codePointAt() (Unicode/Emoji handling)
.charCodeAt() is the “classic” translator from character to number (its Unicode code). But it’s old and it gets confused by emoji! 😵
Think of .charCodeAt() as a translator that doesn’t understand compound words. Emoji (and some rare characters) are often made of two code “pieces” (surrogate pairs). .charCodeAt() sees only the individual pieces and gives you two weird, useless numbers.
"A".charCodeAt(0); // 65
"🎉".charCodeAt(0); // 55357 (wrong!)
"🎉".charCodeAt(1); // 56894 (the other piece)
.codePointAt() is the modern translator. It’s smarter: it understands surrogate pairs and gives you their true, single numeric code.
"A".codePointAt(0); // 65
"🎉".codePointAt(0); // 127881 (Correct!)
Rule: Learn .charCodeAt(), but always use .codePointAt() in modern code to avoid problems with emoji and special characters.
String.fromCharCode() vs String.fromCodePoint()
This is the inverse operation: from number to character.
String.fromCharCode() is the “classic” translator (number ➡️ character). Like charCodeAt(), it doesn’t understand high emoji code points.
String.fromCharCode(65); // "A"
// String.fromCharCode(127881); // ERROR, it doesn’t work or gives weird characters
String.fromCodePoint() is the modern translator. Give it the correct code and it will give you the emoji. It’s a static method, so it’s called on String (capital S).
String.fromCodePoint(65); // "A"
String.fromCodePoint(127881); // "🎉" (Correct!)
Rule: Learn fromCharCode(), but always use String.fromCodePoint().
.startsWith() (String-start check)
This modern method (ES6) checks whether a string starts with another string. It’s preferred because it communicates intent (what you want to do) instead of the steps (how to do it).
const file = "document.pdf";
// MODERN WAY (clear, readable: "Does the string start with...?")
file.startsWith("document"); // true
// CLASSIC WAY (mechanical: "Take the first character...")
file.charAt(0) === 'd'; // true, but less clear and robust
file.slice(0, 9) === "document"; // Works, but verbose
Numbers (Number) - Mathematical Values
Numbers in JavaScript are deceptively simple. There’s no distinction between integers and decimals—everything is a Number. But this simplicity hides some fundamental quirks, like a shiny floor with a few slippery tiles.
Number types and special values (Infinity, NaN)
const integer = 42;
const decimal = 3.14;
const exponential = 5.2e3; // 5200 (scientific notation)
const infinity = Infinity;
const notANumber = NaN; // Not a Number
NaN is a sneaky value: it’s the only value in JavaScript that is not equal to itself (NaN === NaN is false!). That’s why you need specific functions to check for it.
Conversions and Checks
This is where things get interesting. You have several tools to convert and check numbers, each with a different job.
-
isNaN()(The in-depth explanation)Think of
isNaN()(the global one) like a slightly confused customs officer. Its job should be to check whether a value isNaN, but before doing that it tries to forcibly convert it into a number!isNaN(NaN); // true (Obvious)
isNaN("Hi"); // true (Why? It tries Number("Hi") -> NaN. Officer: "Yes, it's NaN!")
isNaN("123"); // false (Why? It tries Number("123") -> 123. Officer: "No, it's 123")
isNaN(undefined); // true (Why? Number(undefined) -> NaN)
// THE TRAP!
isNaN(null); // false (Why? Number(null) -> 0. Officer: "No, it's 0")It’s an unreliable check. For a modern and strict check of whether a value is exactly the
Numbertype and the valueNaN, useNumber.isNaN():Number.isNaN(NaN); // true
Number.isNaN("Hi"); // false (It’s not *already* NaN, it’s a string!) -
Number()(Strict conversion)Number()is an “all or nothing” translator. It tries to convert the entire value. If it fails, it returnsNaN. It’s the strictest and most predictable.Number("123"); // 123
Number("3.14"); // 3.14
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(""); // 0 (Careful!)
// Strict: fails if there is text
Number("42px"); // NaN
Number("Hi"); // NaN -
parseInt()andparseFloat()(Tolerant conversions)These are “extractors”. They’re like garbage collectors that read from left to right and take only the numbers they find at the beginning, throwing away the rest.
parseInt()(Integers only):parseInt("42.5px"); // 42 (Extracts 42, sees "." and stops)
parseInt("age 42"); // NaN (Starts with text, fails immediately)Best Practice: Always use the second argument (the “base” or radix) to tell
parseIntyou’re working in base 10 (our decimal system).parseInt("10", 10); // 10
parseInt("10", 2); // 2 (interprets "10" as binary)parseFloat()(With decimals):parseFloat("42.5px"); // 42.5 (Extracts 42.5, sees "p" and stops)
parseFloat("3.14.15"); // 3.14 (Sees the second "." and stops)
Math - The scientific calculator
The Math object is like having a scientific calculator always available, but built into the language. It’s a static object; you never create it (new Math() doesn’t exist).
-
Math.floor(),Math.ceil(),Math.round()Math.floor(4.9): 4 (Think “floor”. Always rounds down to the lower integer).Math.ceil(4.1): 5 (Think “ceiling”. Always rounds up to the higher integer).Math.round(4.5): 5 (Rounds to the nearest, like in school.4.4->4,4.5->5).
-
Math.random()(Randomness generator)Math.random()generates a pseudo-random number between 0 (inclusive) and 1 (exclusive). It’s like rolling a die with infinite microscopic faces. By itself it’s not very useful, but it’s the foundation for everything.// General formula: integer between min and max (inclusive)
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomInt(1, 6); // A random number between 1 and 6 -
Math.pow()vs the**operatorBoth do exponentiation, but
**is the modern shortcut (ES6+).// Classic way
Math.pow(2, 3); // 8 (2 to the third power)
// Modern way (preferred)
2 ** 3; // 8 -
Math.sqrt()(Readability and intent)For square roots, you have three options.
Math.sqrt()is the best because it communicates intent. Code shouldn’t only work, it should also explain what it does.// 1. Mathematically correct, but “hard” to read
Math.pow(9, 0.5); // 3
// 2. Modern, but requires “knowing” that ** 0.5 is the root
9 ** 0.5; // 3
// 3. The best: clear, readable, self-explanatory
Math.sqrt(9); // 3 (sqrt = SQuare RooT)Write self-explanatory code: use
Math.sqrt()for square roots.
Decimal handling (Floating point)
-
The problem (IEEE-754) Computers “mess up” calculations with decimals. Try typing
0.1 + 0.2in the console: it doesn’t give0.3, but0.30000000000000004. Why? Computers think in binary (base 2). Some simple base-10 numbers (like 0.1, i.e., 1/10) are infinite repeating numbers in binary (for the same reason 1/3 is 0.333... in base 10). The computer has to “cut” them, introducing small precision errors. -
.toFixed()(Rounding to a string) The solution for display is.toFixed(). It rounds the number tondecimal digits. Warning: It returns a STRING, not a number! It’s meant to show the value to the user, not to do more calculations with it.const result = 0.1 + 0.2; // 0.30000000000000004
const display = result.toFixed(2); // "0.30" (a string!) -
parseFloat()(Convert back to a number) If you need to use that rounded number in other calculations (like for currencies), you must convert it back from string to number. This is a fundamental pattern.const subTotal = 100.50;
const taxRate = 0.0825;
// Calculate, round to string, convert back to number
const taxes = parseFloat((subTotal * taxRate).toFixed(2)); // 8.30 (a number!)
const total = subTotal + taxes; // 108.80
3. Dates - The Calendar and the Clock
Dates in JavaScript are complex objects that represent a precise moment in time, measured in milliseconds since January 1, 1970 00:00:00 UTC (the Unix Epoch). They’re notoriously difficult to handle.
Creating dates
const now = new Date(); // Current date and time
const birthday = new Date(2025, 0, 15); // January 15, 2025 (month 0!)
const fromString = new Date("2025-01-15T10:00:00"); // From an ISO string
The tricky methods (getMonth 0-indexed)
Careful! Dates are full of historical traps inherited from other languages.
- THE WORST TRAP:
getMonth()returns the month from 0 to 11. (January is 0, December is 11). getDay()returns the day of the week from 0 to 6. (Sunday is 0, Saturday is 6).getDate()returns the day of the month from 1 to 31 (this one is normal, thankfully).
const today = new Date(2025, 11, 25); // December 25, 2025
today.getMonth(); // 11 (December)
today.getDate(); // 25
today.getDay(); // 4 (Thursday)
It’s an endless source of bugs. Always remember it!
Date.now() (Timestamp)
Date.now() is brilliant in its simplicity. It doesn’t create an object—it returns just a number: the total milliseconds elapsed since 1970. It’s perfect for measuring time, creating unique IDs, or handling expirations.
const start = Date.now();
// ... heavy code to measure ...
const end = Date.now();
console.log(`Operation took: ${end - start}ms`);
4. Booleans (Boolean) - The Binary System of Logic
Booleans are JavaScript’s philosophical bits. Only two values: true or false. They’re the heart of every decision (if, while, ternary) your program makes. They’re like light switches: on or off, yes or no, proceed or stop.
Truthy vs Falsy - The Gray Zone of Truth
JavaScript has this fascinating—and sometimes frustrating—feature: in a boolean context, EVERY value gets “coerced” into becoming true or false. It’s as if JavaScript had special glasses that see everything only in black and white.
The Six Falsy Horsemen (memorize them! These are the ONLY “false” values):
false0(numeric zero)""(empty string)nullundefinedNaN
EVERYTHING else is truthy! Even counterintuitive things:
"0"(true - it’s a string with content!)"false"(true - it’s a string with text!)[](true - an empty array is an object and objects are truthy!){}(true - an empty object exists!)
This allows you to write very concise checks:
const username = ""; // Falsy
if (!username) { // !username is true
console.log("Please, enter a name!");
}
5. Array (Array) - Ordered Lists
Arrays are JavaScript’s ordered collections. Think of them like trains with carriages: each carriage (element) has a number (index), you can add or remove carriages, reorder them, or transform the entire train.
Creation ([] vs Array() (Constructor))
-
[](Literal syntax - Preferred): This is the standard way.const arr = [1, 2, 3]; -
Array()(Constructor): It has a “weird” but useful behavior.Array(1, 2, 3): Creates[1, 2, 3].Array(3): Does NOT create[3]. It creates[ <3 empty items> ](an empty array with 3 empty slots, like an empty egg carton).
Access (0-based index)
Computer science counts from zero. The first element is always at index 0.
const fruits = ["apple", "pear", "banana"];
fruits[0] is "apple". fruits[2] is "banana".
The .length property
fruits.length is 3. It’s a property (no parentheses ()) that indicates the number of elements.
- The last element is always at
fruits.length - 1. - It’s writable:
fruits.length = 0empties the array!
Set and the .size property (For unique values)
A Set is a related data structure, a “VIP club” that accepts only unique values.
new Set([1, 1, 2, 3, 3]) -> Set { 1, 2, 3 }
To count unique elements, you use the .size property (not .length).
const numbers = [1, 1, 2, 3];
numbers.length; // 4
new Set(numbers).size; // 3
Mutating methods (Destructive)
These methods are like surgical operations: they modify the original array. Use them with caution.
-
.push(el): Adds to the end. -
.pop(): Removes from the end. -
.unshift(el): Adds to the start (it’s slow for large arrays!). -
.shift(): Removes from the start (also slow). -
.splice()(The Swiss Army knife) It’s the most powerful and complex method. It can remove, add, or replace.array.splice(startIndex, howManyToRemove, ...itemsToAdd)const letters = ['a', 'b', 'c', 'd'];
// Replace 2 elements ('b', 'c') starting at index 1 with 'X'
letters.splice(1, 2, 'X');
// letters is now: ['a', 'X', 'd'] -
.sort()(Warning: mutates original, default alphabetical) The most famous trap!.sort()sorts alphabetically (as strings) by default.const numbers = [10, 2, 5];
numbers.sort(); // [10, 2, 5] (wrong! "10" comes before "2")
// The fix: the compare function
numbers.sort((a, b) => a - b); // [2, 5, 10] (Correct, ascending)
numbers.sort((a, b) => b - a); // [10, 5, 2] (Descending)
Read-only methods (Non-destructive)
These methods are “gentle”: they create a new array without touching the original. They’re fundamental for functional programming and immutability (a pattern we’ll see).
-
.slice()(Creating copies).slice()is the array “photocopier”.const numbers = [1, 2, 3, 4, 5];
const copy = numbers.slice(); // Photocopies the entire array
const firstTwo = numbers.slice(0, 2); // [1, 2] (index 2 excluded)
// Pattern to sort without destroying:
const sorted = numbers.slice().sort((a, b) => a - b); -
.filter(fn): The “sieve”. Creates a new array with only the elements that pass the test.numbers.filter(n => n > 2); // [3, 4, 5] -
.find(fn): The “detective”. Returns the first element that matches the condition (orundefined).numbers.find(n => n > 2); // 3 -
.findIndex(fn): Returns the index of the first matching element (or-1).numbers.findIndex(n => n > 2); // 2 -
.includes(val): Quick check: “Is this value here?” Returnstrueorfalse. -
.indexOf(val): Where is this value? Returns the index (or-1if not found). -
.join(sep): The “gluer”. Joins an array into a string, using a separator.["a", "b", "c"].join("-"); // "a-b-c"
Functional methods (Iteration/Transformation)
-
.map(fn)(Transformation) The “transformation factory”. Takes an array, applies a function to each element, and returns a new array of the same length with the results.const numbers = [1, 2, 3];
const doubles = numbers.map(n => n * 2); // [2, 4, 6] -
.reduce(fn, initialValue)(Accumulation) The “boiler” or “food processor”. It “cooks down” an entire array to produce a single value (a sum, an object, a string...).const numbers = [1, 2, 3];
// (acc = accumulator, curr = current value)
const sum = numbers.reduce((acc, curr) => acc + curr, 0); // 6The
, 0is theinitialValue. It’s a fundamental best practice to always provide it; otherwisereduceuses the first element as the initial value and skips the first iteration, causing bugs with empty arrays. -
.some(fn)(At least one) Checks whether at least one element passes the test. It’s super efficient: it stops at the firsttrueit finds.numbers.some(n => n > 2); // true -
.every(fn)(All) Checks whether all elements pass the test. It stops at the firstfalseit finds.numbers.every(n => n > 0); // true -
.fill(val)(Filling, a bridge for.map()) As we saw,Array(3)creates[ <3 empty items> ](empty slots)..map()ignores empty slots!.fill()is the “bridge” that turns empty slots into filled slots (e.g.,[undefined, undefined, undefined]), making.map()usable.
Patterns and logic with arrays
-
Pattern: Create a range of numbers This pattern combines
Array(N),.fill(), and.map().const range = (start, end) => {
const length = end - start + 1;
// 1. Create empty slots
// 2. .fill() makes them “mappable” (by filling them with undefined)
// 3. .map() uses the index to create the sequence
return Array(length).fill().map((_, index) => start + index);
};
range(1, 5); // [1, 2, 3, 4, 5] -
Logic: Find the median (Odd and even) (Requires an array that’s already sorted!)
const sortedEvenArr = [1, 2, 3, 4, 5, 6]; // Length 6
const sortedOddArr = [1, 2, 3, 4, 5]; // Length 5
// Odd case (length 5)
const oddIndex = Math.floor(sortedOddArr.length / 2); // floor(2.5) -> 2
const oddMedian = sortedOddArr[oddIndex]; // 3
// Even case (length 6)
const rightCenter = sortedEvenArr.length / 2; // 3
const leftCenter = rightCenter - 1; // 2
const el1 = sortedEvenArr[leftCenter]; // 3
const el2 = sortedEvenArr[rightCenter]; // 4
const evenMedian = (el1 + el2) / 2; // 3.5
6. Objects - Structured containers
Objects are the heart of JavaScript. If arrays are “ordered lists”, objects are “unordered collections” of key-value pairs. They’re like a dictionary or a phone book where every piece of information has a label (the key).
Creation and nested objects
Objects can contain other objects. It’s like having boxes inside other boxes.
const user = {
name: "Mario",
email: "mario@rossi.it",
address: { // Nested object
city: "Rome",
zip: "00100"
},
// Common pattern to group states
keys: {
rightKey: { pressed: false },
leftKey: { pressed: false }
}
};
This is essential for organization. Instead of having scattered variables like userCity, userZip, userRightKeyPressed, you group everything logically.
Access (Dot Notation, Brackets, Optional Chaining ?.)
-
Dot Notation (
.): The most common, clean, and fast.user.name; // "Mario"user.address.city; // "Rome" -
Bracket Notation (
[]): Mandatory in two cases:- The key is a variable:
const key = "name"; user[key]; // "Mario" - The key has special characters:
user["date-of-birth"] = "..."
- The key is a variable:
-
Optional Chaining (
?.): The lifesaver (ES2020)! Prevents errors if an intermediate object doesn’t exist.// Without: ERROR if `user.job` doesn't exist
// const salary = user.job.salary; // Crash!
// With: Safe
const salary = user.job?.salary; // undefined (no crash!)
Shorthand Property Names (ES6)
A super handy syntax shortcut. If the key name you want to create is identical to the variable name holding the value, you can write it only once.
const name = "Mario";
const age = 30;
// Classic:
const classicUser = { name: name, age: age };
// Modern (Shorthand):
const modernUser = { name, age }; // Does the exact same thing!
Destructuring (ES6) - The nesting dolls 🪆
What it does: The inverse operation of creation: it extracts values from an object and “unpacks” them into separate variables in a surgical and clean way.
const product = { id: 1, name: "Book", price: 15 };
// Old way (Verbose):
const oldName = product.name;
const oldPrice = product.price;
// Modern (Destructuring):
const { name, price } = product;
// Now you have two new ready variables: name ("Book") and price (15)
Advanced (Nesting):
const data = {
topic_list: {
topics: ['Post 1', 'Post 2'],
more_topics_url: '...'
},
users: [...]
};
// Old way (Boring):
const topics = data.topic_list.topics;
// Nesting-doll way (Elegant):
// Note the syntax: I use ':' to go down a level
const { topic_list: { topics } } = data;
Analogy: Instead of pulling out the big box, opening it, pulling out the medium one, opening it... with destructuring you teleport straight to the smallest doll!
Best Practice: Renaming (CamelCase vs Snake_case) 🐫 vs 🐍
APIs often speak snake_case (e.g. topic_list), but JS loves camelCase.
Solve the problem right while destructuring:
// Read 'topic_list' from the object, but create a variable called 'topicList'
const { topic_list: topicList } = data;
It’s like having a simultaneous translator while you open the box!
Static methods (Object.keys(), Object.values(), Object.entries())
These are tools to turn an object (which you can’t easily loop with map or filter) into an array (which you can!).
Object.keys(user):["name", "email", "address", "keys"](An array of keys)Object.values(user):["Mario", "mario@rossi.it", {...}, {...}](An array of values)Object.entries(user):[["name", "Mario"], ["email", "mario@rossi.it"], ...](An array of[key, value]pairs)
// Practical use:
const prices = { apple: 1, pear: 2, banana: 1.5 };
// Increase all prices by 10%
Object.entries(prices).forEach(([fruit, price]) => {
prices[fruit] = price * 1.10;
});
hasOwnProperty() vs Object.hasOwn() (Modern)
hasOwnProperty checks whether a property belongs directly to the object (not inherited from the prototype). It’s like checking whether a room is on your house deed or if it’s a shared part of the building.
const user = { name: "Mario" };
user.hasOwnProperty("name"); // true
user.hasOwnProperty("toString"); // false (it’s inherited!)
// Modern (preferred):
Object.hasOwn(user, "name"); // true
Use Object.hasOwn() because it’s a static method and it prevents rare errors where an object might have been created without inheriting hasOwnProperty (e.g. Object.create(null)).
Pattern: Frequency Map (Counter)
A common use of objects is counting occurrences, creating a “frequency map”.
const grades = ["A", "B", "A", "A", "C", "B"];
const count = {};
grades.forEach(grade => {
// The magic is here: (count[grade] || 0)
// If count[grade] exists, use its value
// Otherwise (it’s undefined, falsy), use 0
// Then add 1
count[grade] = (count[grade] || 0) + 1;
});
// count is now: { A: 3, B: 2, C: 1 }
7. Logical Operators and Syntax
If variables are the “containers” and data types are the “shape” of information, operators are the gears and the glue of your program. They’re the verbs that let you combine, compare, transform, and make decisions.
|| (OR) operator for default values
This is one of the most misunderstood yet most useful operators. Many people think || (OR) returns only true or false, but in JavaScript it’s much more powerful: it’s a value selector.
Its logic is: “return the first truthy value you encounter”.
Think of || as a “Plan B”. JavaScript checks the first value. If it’s “good enough” (truthy), it returns it. If it’s “useless” (falsy), then and only then does it return the second value as a fallback.
Remember the Six Horsemen of Falsy (the only “useless” values):
false0""(empty string)nullundefinedNaN
Everything else is truthy (including [] and {}).
// Example 1: Provide a fallback
const userName = "" || "Guest";
// JavaScript sees "" (falsy), so it chooses “Plan B”
// userName is "Guest"
const realUserName = "Mario" || "Guest";
// JavaScript sees "Mario" (truthy), it takes it immediately
// realUserName is "Mario"
// Example 2: The Counter Pattern (FUNDAMENTAL)
// Imagine counting words in a text
const counts = {};
const word = "hello";
// First time we encounter "hello":
// counts[word] is undefined (falsy)
// So (undefined || 0) becomes 0
// And 0 + 1 becomes 1
counts[word] = (counts[word] || 0) + 1;
// counts is now { hello: 1 }
// Second time we encounter "hello":
// counts[word] is 1 (truthy)
// So (1 || 0) becomes 1
// And 1 + 1 becomes 2
counts[word] = (counts[word] || 0) + 1;
// counts is now { hello: 2 }
Modern alternative (??): Careful! Sometimes 0 or "" are valid values you don’t want to discard. In that case, use the “Nullish Coalescing Operator” (??), which triggers only for null or undefined.
! (NOT) operator and the “Toggle” pattern
The ! (NOT) operator is the light switch of logic. It flips a boolean value.
!truebecomesfalse!falsebecomestrue
Like ||, it works with truthy/falsy values. First it “forces” any value to become true or false, and then it flips it.
!true; // false
!false; // true
!"Pizza"; // "Pizza" is truthy, so Boolean("Pizza") is true. !true is false.
!""; // "" is falsy. Boolean("") is false. !false is true.
!0; // 0 is falsy. Boolean(0) is false. !false is true.
!null; // null is falsy. Boolean(null) is false. !false is true.
![]; // [] is truthy. Boolean([]) is true. !true is false.
The “Toggle” pattern (Switch)
This is the most elegant use of !. It lets you invert a boolean state in a single, very readable line.
let isMenuOpen = false; // The menu is closed
function toggleMenu() {
isMenuOpen = !isMenuOpen;
// 1st click: isMenuOpen = !false -> isMenuOpen becomes true
// 2nd click: isMenuOpen = !true -> isMenuOpen becomes false
// 3rd click: isMenuOpen = !false -> isMenuOpen becomes true
// ...and so on, like a light switch.
}
The double NOT trick (!!)
Sometimes you see !!value. It’s not a mistake, it’s a trick! It’s the fastest way to convert any value into its pure boolean equivalent (truthy/falsy).
!!5; // true
!!""; // false
!!{}; // true
Spread operator (...)
The spread operator (or “spreading”) is pure magic. Think of an array or an object as a sealed big box. The three dots ... are the act of opening the box and pouring its contents onto the table, piece by piece.
With arrays (lists)
-
Create copies (immutability):
const original = ["a", "b", "c"];
// const wrongCopy = original; // ERROR! This is just another label for the same box!
const correctCopy = [...original]; // Take a new box and pour the original pieces into it
correctCopy.push("d");
// original is still ["a", "b", "c"] -
Merge (concatenate):
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = [...arr1, 5, ...arr2]; // [1, 2, 5, 3, 4] -
Pass arguments to functions: Some functions (like
Math.max) don’t accept a box (array), they want the single pieces.const numbers = [10, 5, 20];
// Math.max(numbers); // ERROR: NaN
Math.max(...numbers); // Correct! It’s like writing Math.max(10, 5, 20)
With objects (dictionaries)
-
Create copies and update (immutability):
const user = { name: "Mario", age: 30 };
// Copy and update the age
const updatedUser = { ...user, age: 31 };
// { name: "Mario", age: 31 }
// The original is intact!
// Order matters!
const conflictUser = { ...user, name: "Luigi" }; // { name: "Luigi", age: 30 }
const conflictUser2 = { name: "Luigi", ...user }; // { name: "Mario", age: 30 }
Distinction: Spread vs. Rest
Be careful not to confuse “Spread” (which expands) with “Rest” (which collects). The syntax is the same (...), but the context is the opposite.
...in an array/object literal or in a call = Spread (expand)...in function parameters = Rest (collect)function(...args) { /* args is an array of all arguments */ }
Exponentiation operator (**)
This is a modern shortcut (ES6+) for exponentiation. It’s the “shortcut key” on the calculator.
Before, to compute $2^3$ (2 to the third power), you had to use the “scientific calculator” Math:
// Classic way
Math.pow(2, 3); // 8
Math.pow(9, 0.5); // 3 (for the square root)
Now you can use **, which is cleaner, more readable, and integrates with other math operators.
// Modern way (preferred)
2 ** 3; // 8
9 ** 0.5; // 3
Chained assignment
This is syntax you sometimes see to initialize multiple variables to the same value.
let a, b, c;
a = b = c = "value";
console.log(a); // "value"
console.log(b); // "value"
console.log(c); // "value"
How does it work? (The “Gotcha”) Assignment in JavaScript is evaluated right to left, and the whole assignment operation returns the assigned value.
"value"is assigned toc.- The expression
c = "value"returns the value"value". - This value (
"value") is assigned tob. - The expression
b = "value"returns"value". - This value (
"value") is assigned toa.
The common mistake (Wrong syntax) You can’t use logical operators for multiple assignments.
// SYNTAX ERROR!
// a && b = "value"; // Wrong!
This doesn’t work because the left-hand side of an assignment (=) must be a valid reference (like a variable name, e.g. a), not a boolean expression (a && b).
Best Practice: Avoid chained assignment. It’s syntactically cute, but it can be terrible for readability and debugging. Writing assignments on separate lines is almost always better, clearer, and easier to maintain.
// Better like this:
let a = "value";
let b = "value";
let c = "value";
Input/Output and Control Structures
8. Output and Comments
Communicating is essential, not only with the user, but also with yourself and other developers. Your code must “speak”. Output via console is your megaphone during development, while comments are your margin notes, essential for long-term understanding.
console - The control room
Think of the console as the cockpit of your program. It’s not output for your passengers (the users) — for that you’ll use the DOM (see Part III). It’s the control panel for you, the pilot (the developer). It’s the place where the engine tells you whether it’s running well, where you check values on the fly, and where you diagnose problems.
The console.log() method is your main tool. It’s like a “walkie-talkie” that lets you send a message from any point in your code to the cockpit.
// You can pass anything:
console.log("The program has started!");
console.log("Counter value:", counter); // Pass multiple arguments
console.log(user); // Inspect an entire object!
console.log(aDataArray); // Inspect an array
But the cockpit is much more sophisticated than a single log. It has an entire panel of specialized instruments:
// ADVANCED DEBUGGING TOOLS
// 1. Semantic logs (for color and context)
console.log("Normal message");
console.info("Useful information"); // Often with an icon (i)
console.warn("Warning!"); // Yellow ⚠️
console.error("Critical error!"); // Red ❌ (stops execution if it’s a real error)
// 2. Data inspection (fundamental!)
const users = [
{ id: 1, name: "Mario", age: 30 },
{ id: 2, name: "Luigi", age: 28 }
];
console.table(users); // Shows an interactive, sortable table!
// 3. Output organization
console.group("User Validation Start"); // Starts a collapsible group
console.log("Checking name...");
console.warn("Missing email");
console.groupEnd(); // Closes the group
// 4. Performance measurement
console.time("loopTimer"); // Starts a stopwatch called "loopTimer"
for (let i = 0; i < 1000; i++) {
// ...
}
console.timeEnd("loopTimer"); // Stops the stopwatch and prints elapsed time
// 5. Counting
function buttonClicked() {
console.count("buttonClick"); // Prints: "buttonClick: 1", "buttonClick: 2", ...
}
Using console is the art of incremental testing. Instead of writing 100 lines of code and hoping they work, you write 5 and use console.log() to “taste” the result, just like a chef tastes the sauce while cooking.
Comments - Code documentation
Comments are post-its in your code. They are messages for your “future self” or your teammates. Code tells you how it does something, but comments should explain why it does it.
// Single-line comment - for short notes
const tax = price * 0.22; // Apply 22% VAT
/* Multi-line comment
Used for longer explanations, to describe
the complex logic of a function, or to
temporarily disable a block of code
without deleting it.
*/
/*
function oldFunction() {
console.log("This is no longer needed");
}
*/
JSDoc - Formal documentation
When you write a function or a class, using the /** ... */ format (JSDoc) is a professional best practice. It’s not just a comment — it’s documentation that your editor (and other tools) can read to give you automatic hints.
/**
* JSDoc - Formal documentation
* @param {number} price - The base price of the item
* @param {number} discount - The discount percentage (e.g. 20)
* @returns {number} The final discounted price
*/
function applyDiscount(price, discount) {
return price * (1 - discount / 100);
}
Special tags - Organizing the work
Use standard tags to create an internal “to-do list” inside the code.
// TODO: Implement email validation with a regex
// FIXME: This doesn’t handle negative numbers, it crashes
// NOTE: The API requires ISO date format (YYYY-MM-DD)
// HACK: Added a small timeout to wait for the CSS animation (400ms)
// DEPRECATED: Use the new function `calculateTotalV2()` from v2.0
Best practices for comments: Explain the “Why”, not the “What”
Comments shouldn’t be an echo of the code. They should add value.
// BAD: Obvious, useless comment
let count = 0; // Set count to 0
// GOOD: Explains the “why” and the context
let count = 0; // Failed-attempt counter (max 3 before account lockout)
A good comment is like a margin note in a difficult book: it doesn’t repeat the text, it gives you the key to understand it.
9. Flow Control - The Program’s Decisions
If code were a recipe, so far we’ve only seen the ingredients (data types) and the tools (operators). Flow control is the recipe itself: it’s the sequence of steps, the decisions, the “ifs” and “elses” that turn a static list of instructions into a dynamic and intelligent program. It’s the point where your code stops being a rock and starts being a robot.
if/else - The Classic Fork
Think of if as a fork in the road. Your program arrives at the fork and has to make a decision. The condition inside the parentheses () is the road sign the program reads.
// The condition is the question
if (condition) {
// ...code block if the condition is 'true'
}
The crucial part to understand is that condition is always coerced into a boolean. This is where the concepts of Truthy and Falsy become fundamental.
const username = "Mario";
if (username) { // "Mario" is truthy, so the code runs
console.log(`Welcome, ${username}`);
}
const score = 0;
if (score) { // 0 is falsy, so the code does NOT run
console.log("You have a score!");
}
-
if / else - The Two-Way Fork If
ifis the fork,elseis the other road. It’s the guaranteed "Plan B". If theifcondition is false, theelseblock runs.const age = 15;
if (age >= 18) {
console.log("Access granted: Adult");
} else {
console.log("Access denied: Minor");
} -
if / else if / else - The Roundabout with Multiple Exits When you have more than two choices, you can chain
else ifstatements to create a sequence of checks.const score = 85;
if (score > 90) {
console.log("A");
} else if (score > 80) {
console.log("B"); // It goes here!
} else if (score > 70) {
console.log("C");
} else {
console.log("F");
}JavaScript runs the checks in order and stops at the first one that is
true.
Ternary Operator - The Compact Fork (for Assignments)
The ternary operator is an ultra-compact version of if/else. Think of if/else as a formal letter, and the ternary as a sticky note.
Its syntax is: condition ? value_if_true : value_if_false;
It’s designed to return a value, so it’s perfect for assigning data to a variable.
const age = 20;
// Classic way
let status;
if (age >= 18) {
status = "Adult";
} else {
status = "Minor";
}
// Ternary way (cleaner)
const ternaryStatus = age >= 18 ? "Adult" : "Minor";
// Read it as: "Is the age >= 18? If yes, use 'Adult', otherwise use 'Minor'."
When should you use it? Use it only for simple assignments. If you start “nesting” ternaries (a ternary inside another), you’re creating an unreadable monster. For complex logic, if/else is always the better choice for clarity.
switch - The Telephone Switchboard
If if/else is a roundabout, switch is a telephone switchboard. It’s perfect when you have one single variable (the “extension” you want to call) to compare against a list of static values (the “available extensions”). It’s much cleaner than a long chain of if (x === 1) else if (x === 2) else if (x === 3)....
const action = "save";
switch (action) { // 1. The value to check
case "save": // 2. "Does it match 'save'?"
console.log("Data saved.");
break; // 3. FUNDAMENTAL: exit!
case "load":
console.log("Loading...");
break;
case "delete":
case "remove": // 4. "Fall-through": group multiple cases
console.log("Item deleted.");
break;
default: // 5. The switch’s "else"
console.log("Unknown action.");
}
The Secrets of switch:
breakis your best friend: Forgettingbreakis the most common mistake. Without it, the code “falls through” and also runs the next case, and the one after that, until it finds abreakor reaches the end. It’s like a switchboard operator who connects you to the right office, but forgets to disconnect the previous one, and now you’re in a conference call with the whole building.- Strict comparison (
===):switchuses strict identity comparison (like===). This meansswitch(5)will not matchcase "5", because a number is not a string.
"Return Early" Pattern - The Guard at the Door
This isn’t a command, but a pattern, a way of writing cleaner and more robust code. Think of this technique as a security guard (or “bouncer”) at the entrance of a function.
The “classic” way to write a function is to check positive conditions, creating a “pyramid of doom” of nested ifs.
// BAD: The pyramid 👎
function processPayment(user, cart) {
if (user) {
if (user.hasValidCreditCard) {
if (cart.total > 0) {
// ...finally, the code we care about...
// ...buried under 3 levels of indentation...
executePayment(cart.total, user.card);
} else {
console.error("Empty cart");
}
} else {
console.error("Invalid card");
}
} else {
console.error("User not logged in");
}
}
This code is hard to read. The “happy path” (the one that does the real work) is hidden at the bottom.
The Return Early pattern (or Guard Clauses) flips the logic: check all the negative conditions first and exit immediately (return) if something is wrong.
// GOOD: Flat and readable (Guard Clauses) 👍
function processPayment(user, cart) {
// 1. Guard: does the user exist?
if (!user) {
console.error("User not logged in");
return; // Exit immediately
}
// 2. Guard: is the card valid?
if (!user.hasValidCreditCard) {
console.error("Invalid card");
return; // Exit immediately
}
// 3. Guard: is the cart full?
if (cart.total <= 0) {
console.error("Empty cart");
return; // Exit immediately
}
// If we got here, everything is valid.
// The "happy path" is flat and easy to read.
executePayment(cart.total, user.card);
}
This style is immensely superior because:
- It reduces indentation.
- It makes the function’s main logic immediately visible.
- It handles all edge cases at the start, like a bouncer filtering the line.
10. Loops - Automated Repetitions
Loops are the essence of automation. They’re how you tell the computer: “Do this thing, then do it again, and again... until I tell you to stop.” Without loops, you’d have to write console.log(1), console.log(2)... a thousand times. With a loop, you write it just once.
Think of loops as different kinds of worker robots, each specialized for a different task.
for - The Counter Loop (The Industrial Robot)
The for loop is your industrial robot on a conveyor belt. It’s precise, methodical, and it knows exactly what it has to do before it even starts. It’s perfect when you know in advance how many times you need to repeat an action.
Its syntax is like its control panel, with three fundamental settings:
for (initialization; condition; increment) { ... }
- Initialization (
let i = 0): The “starting point”. The robot sets its counter to 0. This variablei(stands for “index”) lives only inside the loop, thanks to the Block Scope oflet. - Condition (
i < 5): The “work limit”. Before every single round, the robot checks: “Is my counter still below 5?”. If yes, it works. If no, it stops. - Increment (
i++): The “after work” action. After completing a round, the robot presses the button to move the conveyor forward and increments its counter (ibecomes 1, then 2, etc.).
// Print the numbers from 0 to 4
for (let i = 0; i < 5; i++) {
console.log(`Iteration ${i}`);
}
// Output: 0, 1, 2, 3, 4
When to use it? It’s the perfect choice for iterating over an array when you need the index:
const arr = ["a", "b", "c"];
for (let i = 0; i < arr.length; i++) {
console.log(`Index ${i}: ${arr[i]}`);
}
while - The Conditional Loop (The Night Guard)
If for is an industrial robot, while is a night guard. It doesn’t know how many rounds it will do tonight. It only knows it must “keep patrolling while (while) the main door is closed”.
It checks the condition before doing anything.
while (condition) { ... }
- Check the
condition. - If it’s
true, execute the code block. - Go back to step 1 and check again.
The Danger: The Infinite Loop!
The night guard must have a way to change the condition. If the door never opens (and the guard doesn’t have a key), it will patrol forever. You must always make sure that something inside the while (or outside) eventually makes the condition false.
let attempts = 0;
let enteredPassword = "";
while (enteredPassword !== "secret" && attempts < 3) {
console.log(`Attempt ${attempts + 1}`);
// Something MUST change the condition
enteredPassword = prompt("Enter password:"); // Changes the condition
attempts++; // Changes the condition
}
if (enteredPassword === "secret") {
console.log("Access granted!");
} else {
console.log("Account locked.");
}
When to use it? When you don’t know how many iterations will be needed, but you do know the condition you must stop at. (E.g., “keep downloading data until there’s nothing left”, “keep asking the user until they answer ‘yes’”.)
do...while - The Guaranteed Loop (Do First, Ask Later)
This is the impulsive cousin of while. It’s a night guard who does at least one patrol round before even checking whether the door is closed. It’s “shoot first, ask questions later”.
do { ... } while (condition);
- Execute the
doblock (the first time, always! No questions asked). - Then, at the end of the round, check the
condition. - If it’s
true, go back to step 1 and repeat.
When to use it? When you need to execute the action at least once, regardless of the condition. It’s the undisputed king for creating interactive menus.
let choice;
do {
console.log("--- MENU ---");
console.log("1. Play");
console.log("2. Options");
console.log("3. Exit");
choice = prompt("Choose an option (1-3):");
// ... logic for choices 1 and 2 ...
} while (choice !== "3"); // Keep showing the menu until they choose "3"
console.log("Goodbye!");
for...of - The Loop for Collections (The Elegant Explorer)
This is the modern and elegant way to loop. It’s like having a magic box (const cart) and telling JavaScript: “examine each item (const product) in (of) the box, one at a time; I don’t care about order or index—just give them to me”.
for (const element of iterable) { ... }
- It works magically on “iterables”: Arrays, Strings, Map, Set, and DOM
NodeLists. - It does not work on plain objects
{}(they’re not iterable in this way).
Advantages:
- Readability:
for (const product of cart)is much cleaner thanfor (let i = 0; i < cart.length; i++) { ... }. - Safety:
const elementprevents accidental modifications. - No Indices: You don’t have to worry about
i,length, or messing up withi <= length.
// Example 1: Iterate over an Array
const cart = ["apples", "bread", "milk"];
for (const product of cart) {
console.log(`Buy: ${product}`);
}
// Example 2: Iterate over a String
for (const letter of "Hi") {
console.log(letter); // Prints H, i
}
Fundamental distinction: for...of vs for...in
Don’t confuse them!
for...ofiterates over the values of an iterable (Array, String...). It’s what you want 99% of the time.for...initerates over the keys (properties) of an object.
const obj = { a: 1, b: 2 };
for (const key in obj) {
console.log(key); // Prints "a", "b" (the keys!)
}
forEach - The Array Iterator (The Team Lead)
forEach isn’t a “native” JavaScript loop (like for or while), it’s a method that exists only on Arrays. It’s like a team lead who says: “For each (forEach) worker (element) on my team (array), tell them to do this task (the callback).”
array.forEach(function(element, index) { ... });
- It takes a callback (an instruction) that gets executed for each element.
- The callback automatically receives
(element, index, fullArray)as arguments.
const fruits = ["apple", "pear", "banana"];
fruits.forEach((fruit, index) => {
console.log(`${index + 1}. ${fruit}`);
});
// Output:
// 1. apple
// 2. pear
// 3. banana
The “Flaw” (or Feature): You Can’t Stop It!
forEach is like a train that must visit every station. The break and continue commands DO NOT WORK inside it. If you need to stop halfway (for example, to search for an element), don’t use forEach. Use a classic for loop, for...of, or the .find()/.some() methods.
Flow Control in Loops (The Teleporters)
break and continue are the loop’s “teleporters”. They’re emergency commands for your robot. They work in for, while, and do...while (but not in forEach).
break - The Emergency Stop Button 🚨
- What it does: Stops immediately the entire loop (the innermost one it’s in) and “jumps” out, continuing execution of the code after the loop.
- Analogy: It’s the red emergency button. It doesn’t matter how many bolts are left—the robot stops and the conveyor belt shuts down.
// Find the first even number and stop
const numbers = [1, 3, 5, 6, 7, 9, 8];
let firstEven;
for (const num of numbers) {
if (num % 2 === 0) {
firstEven = num;
break; // Found it! Exit the loop. Don’t keep searching.
}
}
// firstEven is 6 (not 8)
continue - Skip to the Next Round ⏭️
- What it does: Stops only the current iteration and “jumps” immediately to the start of the next round (to the increment
i++in afor, or to the checkwhile (condition)). - Analogy: It’s the “discard this piece” command. The robot sees a defective bolt, throws it away (
continue), and immediately moves on to the next bolt on the conveyor belt, without completing the other operations for the defective one.
// Print only odd numbers
for (let i = 0; i < 10; i++) {
if (i % 2 === 0) { // If it’s even...
continue; // ...skip this round, don’t run console.log.
}
// This code runs only if `continue` didn’t trigger
console.log(i); // 1, 3, 5, 7, 9
}
Functions and Scope
11. Functions - Reusable Code Recipes
Functions are the fundamental building blocks of a well-organized program. They’re like recipes: you define a series of steps only once (e.g., “how to make a cake”) and then you can “cook” that result whenever you want, simply by calling the recipe and providing the ingredients (the “parameters”).
They are your main tool for applying the DRY (Don't Repeat Yourself) principle. If you find yourself writing the same block of code more than once, it’s time to turn it into a function.
Classic Declarations vs Arrow Functions
There are two main ways to write a function, each with its own characteristics.
-
Classic Declaration (
function) This is the traditional, robust, universal way. Think of it as the formal recipe written on parchment.// Classic declaration
function greet(name) {
return `Hi, ${name}!`;
}
// Function expression (almost identical, but assigned to a variable)
const greetAsExpression = function(name) {
return `Hi, ${name}!`;
};Key features:
- They are “lifted” (hoisted): You can call a classic function before defining it in the code.
- They have their own
thisvalue, which changes depending on how and where they are called (this is an advanced topic and often a source of confusion).
-
Arrow Functions (
=>) Introduced in ES6, they’re the modern, concise, and often preferred way. Think of them as a quick note on a sticky note.const greet = (name) => {
return `Hi, ${name}!`;
};Key features:
- Concise syntax: Less “noise” (no
functionkeyword). - No own
this: They don’t have their ownthis! They “inherit” it from the context where they were created. This solves huge headaches, especially withaddEventListenerand class methods.
- Concise syntax: Less “noise” (no
Implicit vs Explicit Return
This is one of the superpowers of Arrow Functions.
-
Explicit Return (With
{}) If your arrow function uses curly braces{}, you’re defining a “code block” (which can contain multiple lines). Just like in a classic function, you must use thereturnkeyword to return a value.// Explicit (with braces, you need return)
const sum = (a, b) => {
const result = a + b;
return result; // You must write 'return'
}; -
Implicit Return (Without
{}) If your function does only one thing (a single expression, a “one-liner”), you can omit both the braces{}and thereturnkeyword. JavaScript will automatically return the result of that single expression.// Implicit (without braces, automatic return)
const sum = (a, b) => a + b;
// Perfect for array methods
const doubled = [1, 2, 3].map(n => n * 2); // [2, 4, 6]
Returning an Object (with ())
The Fundamental Trap! What happens if you want to return an object implicitly?
// WRONG! ❌
const createUser = (name) => { name: name, age: 30 };
// This returns 'undefined'!
Why? JavaScript sees the { and thinks it’s the start of a code block (explicit return), not an object literal.
The Solution: Wrap the object in parentheses ().
This tells JavaScript: “Hey, treat what’s inside the braces as a single expression (an object), not as statements.”
// CORRECT! ✅
const createUser = (name) => ({ name: name, age: 30 });
// This correctly returns the object { name: "Mario", age: 30 }
It’s like putting a label on the box that says: “This is an object, not a list of commands.”
Default Parameters
A clean (ES6+) way to make your functions more robust, providing “fallback values” for parameters that aren’t passed.
Analogy: A recipe that says “a pinch of salt (or 1g if you don’t know what a ‘pinch’ is)”.
// Before you had to do it like this (clunky)
function greetOld(name) {
name = name || "Guest";
return `Hi, ${name}`;
}
// Now (much cleaner)
function greet(name = "Guest", timeOfDay = "morning") {
return `Good${timeOfDay}, ${name}!`;
}
greet(); // "Goodmorning, Guest!"
greet("Mario"); // "Goodmorning, Mario!"
greet("Mario", "evening"); // "Goodevening, Mario!"
Destructuring in Parameters
This is an advanced but incredibly clean pattern. It lets you “unpack” (destructure) an object or an array directly in the function signature.
Analogy: Instead of receiving an entire fruit basket (the user object) and then having to pull out the apple (user.name) and the banana (user.age), you tell the function: “I only need the apple and the banana, give me those directly.”
const user = { id: 1, name: "Mario", age: 30 };
// Without destructuring (clunky)
function introducePerson(u) {
return `${u.name} is ${u.age} years old`;
}
// With destructuring (clean!)
function introducePerson({ name, age }) {
return `${name} is ${age} years old`;
}
introducePerson(user); // "Mario is 30 years old"
// The best: destructuring + default parameters
function configure({ theme = "light", volume = 50 } = {}) {
// ...
}
configure(); // Works without errors, using defaults
Callbacks (Basic Concept)
This is a fundamental concept in JavaScript. A callback is a function that is passed as an argument to another function, with the intention of being “called back” at a later time.
Analogy: Ordering Pizza 🍕
- You call the pizza place (the
orderPizzafunction). - You don’t stay on the phone waiting for the pizza to be ready (the code doesn’t block).
- You give the pizza maker your phone number (the callback).
- When the pizza is ready (the event), the delivery person calls you (executes the callback).
Why is it Fundamental?
-
Asynchrony (The Pizza): For operations that take time (downloads, timers). It allows your code to “keep doing other things” while it waits.
console.log("Ordering...");
// setTimeout is a function that takes a callback and a time
setTimeout(() => { // This is the callback (your number)
console.log("🍕 Pizza arrived!"); // Executed after 2 seconds
}, 2000);
console.log("Meanwhile I set the table."); -
Specialization (The Machine): To tell a generic function what to do.
mapis a generic machine that “walks an array”. Your callback is the specific instruction (e.g., “double the number”) to execute at each step.const numbers = [1, 2, 3];
const double = n => n * 2; // This is the callback (the instruction)
const doubled = numbers.map(double); // [2, 4, 6]
Currying
Currying is a functional programming technique that transforms a single function with multiple arguments (e.g., fn(a, b, c)) into a sequence of functions, each with only one argument (e.g., fn(a)(b)(c)).
Analogy: The Specialized Chef 👨🍳
prepareDish(ing1, ing2, ing3): It’s a chef who needs all 3 ingredients right away to start cooking.prepareDishCurried(ing1): You give the chef the first ingredient (e.g., “pasta”). He doesn’t give you the finished dish. Instead, he gives you a new specialized chef who now only knows how to make pasta-based dishes.pastaChef(ing2): You give this new chef “tomato”. He gives you an even more specialized chef who can only make pasta with tomato and is waiting for the last ingredient.
Advantage (Partial Application)
The real power isn’t calling fn(a)(b)(c) all at once. It’s saving the specialized chefs! This is called “Partial Application”.
// "Curried" function
const curriedAdd = (a) => { // The first chef
return (b) => { // The specialized chef
return a + b;
};
};
// --- Partial Application ---
// Let’s create a specialized chef!
const add10 = curriedAdd(10);
// add10 is now a *new function* (b => 10 + b)
// It’s a "chef" that has 10 "locked" inside itself.
// Now we use our specialized chef whenever we want
console.log(add10(5)); // 15
console.log(add10(20)); // 30
console.log(add10(90)); // 100
It’s incredibly useful for creating reusable and configurable functions.
How it works (Closure): This is possible only thanks to Closure. The inner function (b => ...) “remembers” the value of a even after the outer function has finished executing.
Syntax (Arrow Function)
Currying and Arrow Functions (with implicit return) are made for each other.
// The long version (with explicit returns)
const curriedAddLong = (a) => {
return (b) => {
return a + b;
};
};
// The "compressed" version with arrow functions
const curriedAdd = a => b => a + b;
// They work the same way!
const add5 = curriedAdd(5);
console.log(add5(3)); // 8
Underscore (_) Convention for Unused Parameters
This isn’t a JavaScript rule, but a stylistic convention (a “gentlemen’s agreement” among programmers).
Sometimes, a function (especially a callback) provides you with more parameters, but you only need a later one.
Example: arr.map((element, index) => ...)
And what if you wanted only the index and not the element? You can’t write arr.map((index) => ...) because JavaScript will think index is the first parameter (the element).
Analogy: The Mail 📬
You have to check the mail to find the one important bill. The mailbox contains (advertising, bill, magazine).
You’re forced to grab the advertising to reach the bill.
The Solution: Still define the parameter, but name it _ (or _element) to signal to the reader (and to code analysis tools) that: “Yes, I know this parameter exists, but I intentionally ignored it.”
// I want to create an array of indices [0, 1, 2]
const arr = ["a", "b", "c"];
// I don’t need the value ("a", "b", "c"), only the index
const indices = arr.map((_element, index) => {
return index;
});
// indices is [0, 1, 2]
// The '_element' says "I’m ignoring the first parameter, it’s not a bug"
12. Scope - Variable Visibility
Scope (or visibility scope) is the set of rules that determines where a variable is accessible in your code. It’s not an abstract concept—it’s a physical rule of the language, like gravity.
Think of your program as a big house (Global Scope). Inside this house, there are many private rooms (Function Scope), and inside those rooms there are locked closets (Block Scope).
Scope answers the question: “If I’m in this room, which variables can I see and use?”
Global Scope vs Local Scope
This is the fundamental distinction—the difference between the town square and your living room at home.
-
Global Scope (The Square) A variable is in the Global Scope if it’s declared outside of any function or block.
// THESE ARE GLOBAL
let maxScore = 1000;
const GAME_NAME = "JS Adventure";
function showScore() {
console.log(maxScore); // I can see it from here!
}Analogy: It’s a monument in the main square. Anyone, from any window of any building (function), can lean out and see it. The Danger: It’s also “pollution”. If too many things are global, anyone can also try to modify them (if they’re
let). It’s like leaving your wallet on a public bench: convenient, but dangerous. -
Local Scope or Function Scope (The Living Room) A variable is in Local Scope if it’s declared inside a function.
function play() {
// THIS IS LOCAL
let roundScore = 100;
console.log(`You scored ${roundScore} points!`);
// I can also see the global from here
console.log(`The max score is ${maxScore}`);
}
play();
// console.log(roundScore); // ERROR! ❌
// ReferenceError: roundScore is not definedAnalogy:
roundScoreis the remote control in your living room. It exists only in that room. Anyone outside (Global Scope) can’t see it, can’t use it, and doesn’t even know it exists. When you leave the room (the function ends), the remote “disappears” (the variable is destroyed by the Garbage Collector).
Scope Chain - The Visibility Chain
Okay, but how does JavaScript decide which variable to use? It follows a process called the Scope Chain (Visibility Chain).
Think about when you look for your house keys:
- You check your pockets (the Current Scope, the most inner one). Find them? Perfect—you stop and use them.
- Not in your pockets? You check the living-room table (the Outer Scope, the function that contains you). Find them? Okay, use those.
- Not on the table? You check the coat rack by the entrance (the Global Scope). Found them? Use those.
- Not even there? You give up. (JavaScript throws a
ReferenceError).
JavaScript does exactly the same thing. It looks for the variable in its current scope and, if it can’t find it, it “climbs” the scope chain, one link at a time, until it reaches the Global one.
The Concept of "Shadowing" What happens if you have two variables with the same name at different levels? The closest one wins!
const message = "Global"; // 3. Coat rack by the entrance
function outer() {
const message = "Outer"; // 2. Living-room table
function inner() {
const message = "Inner"; // 1. In your pockets
console.log(message); // Looks... and finds it immediately!
}
inner(); // Output: "Inner"
console.log(message); // Looks... finds it on the "table"
}
outer(); // Output: "Outer"
console.log(message); // Looks... finds it at the "entrance"
// Output: "Global"
The variable message = "Inner" “shadows” the outer one, and the outer one shadows the global one.
Block Scope with let and const
This is the modern revolution (introduced with ES6).
- Before, only
functions created a “room” (Function Scope). - Now, with
letandconst, any pair of curly braces{}creates a “closet” (a Block Scope).
This includes if, else, for, while, or even just braces placed randomly.
var (the old way) IGNORES blocks:
if (true) {
var x = 10;
}
console.log(x); // 10
// 'x' “escaped” from the if block! It’s as if the closet had no door.
let and const RESPECT blocks:
if (true) {
let y = 20;
const z = 30;
}
// console.log(y); // ERROR! y is not defined
// console.log(z); // ERROR! z is not defined
// 'y' and 'z' are locked inside the `{}` closet.
Why this is FUNDAMENTAL: for Loops
This is the example that makes everything click. Look at the classic “bug” with var:
// The classic var "bug"
for (var i = 0; i < 3; i++) {
setTimeout(() => {
// When this code runs, the loop has ALREADY FINISHED.
// The variable 'i' was hoisted
// and its final value is 3.
console.log(i);
}, 100);
}
// Output: 3, 3, 3
This happens because there’s only one i for the entire loop, living in the function scope.
Now look at the magic of let:
// With 'let', each loop round creates a NEW 'i'
for (let i = 0; i < 3; i++) {
// Each 'i' (0, 1, 2) is a different copy,
// "frozen" in the scope of its specific loop round (thanks to closure)
setTimeout(() => {
console.log(i);
}, 100);
}
// Output: 0, 1, 2 (As you’d expect!)
Using let in for loops saves you from unimaginable headaches. It’s like having a separate closet for every single round of the conveyor belt.