Skip to main content

JavaScript Real World Vademecum

Part V: Advanced Patterns & Algorithms

The difference between code that works and code that scales lies here. We explore advanced algorithms, Regex, design patterns, and optimization techniques for professional-grade development.


Patterns and Best Practices

Writing code that works is the first step. Writing code that is good is the final goal. “Good” code is clean, readable, easy to maintain, and hard to break. This section collects the “patterns” (models) and “best practices” (habits) that turn a programmer into a professional.

34. Development Patterns

Accumulation Pattern

Accumulation is one of the most common patterns. It’s like filling a bucket drop by drop. You start with an empty “container” (whether it’s a number, a string, or an array) and then, inside a loop, you “accumulate” results.

// 1. Numeric accumulation (Sum)
let total = 0; // The empty bucket
const prices = [10, 20, 30];
for (const price of prices) {
total += price; // Add each "drop"
}
// total is now 60

// 2. String accumulation (Building)
let html = "<ul>"; // The starting container
const fruits = ["Apple", "Pear"];
for (const fruit of fruits) {
html += `<li>${fruit}</li>`; // Accumulate string chunks
}
html += "</ul>";
// html is now "<ul><li>Apple</li><li>Pear</li></ul>"

// 3. Array accumulation (Filtering)
const positives = []; // The empty array
const numbers = [-1, 10, -5, 20];
for (const num of numbers) {
if (num > 0) {
positives.push(num); // Accumulate only positives
}
}
// positives is now [10, 20]

The .reduce() method (as seen in Part I, chapter 5) is the functional and compact version of the accumulation pattern.

Boolean Flags - The Switches

A “flag” is a light switch. It’s a boolean variable (true/false) that you use to remember a state and control the program flow.

Analogy: You’re looking for your keys in a drawer. You keep a finger up (the flag found = false). As soon as you find them, you lower the finger (found = true) and stop searching.

let isLoading = false; // Flag: "Are we loading data?"
let hasError = false; // Flag: "Was there an error?"

function fetchData() {
isLoading = true;
showSpinner(); // Show the loading icon

// ...simulate network call...
setTimeout(() => {
if (operationFailed) {
hasError = true;
}
isLoading = false;
hideSpinner();
updateUI();
}, 2000);
}

State Variables

This is the evolution of a boolean flag. Instead of a simple true/false, a state variable tracks which “mode” your application is in.

Analogy: A traffic light. It’s not just “on/off”, it has precise states: "red", "yellow", "green".

// States of a form
let formState = "editing"; // Possible states: "editing", "submitting", "submitted", "error"

function handleForm() {
switch(formState) {
case "editing":
enableFields();
hideSpinner();
break;
case "submitting":
disableFields();
showSpinner();
break;
case "submitted":
showSuccessMessage();
break;
case "error":
showErrorMessage();
enableFields();
break;
}
}

Using a state variable prevents bugs, like allowing the user to click “Submit” (submitting) while it’s already submitting.

Configuration Objects Pattern - The Control Panel

This pattern consists of grouping all your settings and “magic numbers” into a single const object at the top of the file.

Analogy: Instead of having sticky notes with passwords and settings scattered all over the office, you keep them all in a single control panel locked up.

// BAD: Scattered magic numbers
function checkAttempts(attempts) {
if (attempts > 3) { ... }
}
fetch("https://api.example.com/v1/users");

// GOOD: Configuration object
const CONFIG = {
API_URL: "https://api.example.com/v1",
MAX_RETRIES: 3,
TIMEOUT_MS: 5000,
MESSAGES: {
error: "An error occurred",
loading: "Loading..."
}
};

// Now the code is clean and maintainable
function checkAttempts(attempts) {
if (attempts > CONFIG.MAX_RETRIES) { ... }
}
fetch(`${CONFIG.API_URL}/users`);

If one day the API changes or you want to change the maximum number of retries, you edit one place.

Error Handling with try-catch

Code will fail. It’s a certainty. try-catch is your safety net.

Analogy: You’re a trapeze artist. try is your acrobatic act. catch is the safety net under you. You can attempt the jump (risky code) without fear of smashing into the ground (crashing the entire application).

function parseJSON(jsonString) {
try {
// 1. Try to run this risky code
const data = JSON.parse(jsonString);
console.log("Parsing succeeded:", data);
return data;

} catch (error) {
// 2. If *anything* in 'try' fails,
// execution jumps here immediately.
console.error("ERROR! Invalid JSON:", error.message);
// 'error' is an object that contains error details
return null; // Return a safe value

} finally {
// 3. (Optional) Runs *always*,
// whether try succeeds or catch triggers.
// Useful for cleanup, e.g., hiding a spinner.
console.log("Parsing attempt completed.");
}
}

parseJSON('{"name": "Mario"}'); // Succeeded
parseJSON('{name: "Mario"}'); // Fails (missing quotes on the key), but doesn’t crash!




35. Advanced UX: Performance and Adaptability

Don’t treat all users the same. A user with the latest iPhone connected to home fiber has superpowers that a user on an old Android in a shopping mall doesn’t. Your code must adapt.

A. navigator.connection - The browser’s “sense of touch” 📶

What it does: Lets your code “feel” the quality of the user’s connection and decide how heavy the data to download should be.

const connection = navigator.connection;

// Detect whether the user wants to save data or has a slow connection
const isSlow = connection ? (connection.saveData || connection.effectiveType.includes('2g')) : false;

const itemsToLoad = isSlow ? 5 : 20; // 5 items if slow, 20 if fast
const imageQuality = isSlow ? 'low' : 'high'; // Pixelated but fast images vs HD

console.log(`Loading ${itemsToLoad} items in ${imageQuality} quality`);

Analogy: Netflix 📺 Have you noticed that if the network slows down, Netflix doesn’t stop but lowers the video quality (it gets a bit blurry)? Here, you’re doing the same thing: instead of blocking the user, you give them a “light” but working experience.

B. Intersection Observer - “Infinite scroll” ♾️

What it does: Instead of forcing the user to click “Load more” (friction), it automatically loads new content when the user reaches the bottom of the page.

How it works: You create a “sentinel” (an invisible element) at the end of the list. When this sentinel enters the viewport, loading triggers.

// 1. The sentinel (at the bottom of the HTML)
// <div id="sentinel"></div>

// 2. The Observer
const observer = new IntersectionObserver((entries) => {
// If the sentinel is visible...
if (entries[0].isIntersecting) {
console.log("We’re at the bottom! Load new posts...");
fetchMoreData(); // Your function that does the fetch
}
});

// 3. Start observing
const sentinel = document.getElementById('sentinel');
observer.observe(sentinel);

Analogy: The Truman Show (or an open world video game) 🌍 The whole world doesn’t exist all at once. The world is “built” only a moment before the protagonist lays eyes on it. If you don’t look, it doesn’t exist. This saves enormous resources!

Golden rule:

  • “Load more” button: Safe but boring (High friction).
  • Infinite scroll: Modern and smooth (Zero friction), but you must manage memory well!




36. Style and Quality Best Practices

Best Practices - Naming Convention

Names in your code are as important as the code itself. They must tell a story. A well-chosen name removes the need for a comment.

Analogy: It’s the difference between labeling a box “STUFF” and labeling it “2023 Electronic Invoices”.

  • Global constants (Configuration): UPPER_SNAKE_CASE (All caps, with underscores). const MAX_ATTEMPTS = 3; const API_KEY = "abc123";

  • Variables and Functions: camelCase (Starts lowercase, each new word capitalized). let userName = "Mario"; function calculateTotal() {}

  • Classes (Blueprints): PascalCase (Starts with a capital letter). class UserAccount {} class ShoppingCart {}

  • Semantic names (that “speak”):

    • Booleans (Flags): Start like questions: isVisible, hasPermission, canEdit.
    • Functions: Verbs that describe the action: fetchData(), validateEmail(), renderComponent().
    • Arrays: Plural names: users, products, items.
    • Objects: Descriptive singular names: user, product, configuration.

Incremental Testing

Analogy: Tasting the sauce while you cook. Don’t write 100 lines of code and then press “play” hoping everything works. It’s a recipe for disaster.

The professional workflow is write-test-write-test:

  1. Write 3 lines (e.g., an empty function).
  2. Test (console.log("Function called")).
  3. Write 5 more lines (the internal logic).
  4. Test (console.log("Intermediate result:", result)).
  5. Finish the function.
  6. Test (console.log("Final result:", final)).

console.log is your most powerful debugging tool. Use it. Always.

Separation of Concerns (SoC)

This is a fundamental design principle. Every “piece” of your code (function, class, module) must have one single, clear responsibility.

Analogy: In a restaurant, the chef cooks, the waiter takes orders, the cashier handles money. It’s a disaster if the chef also has to take orders and clean tables.

// BAD: The “do-everything” function 👎
function processUserData(userData) {
// 1. Validate...
if (!userData.email) return false;
// 2. Save...
database.save(userData);
// 3. Send email...
sendEmail(userData.email);
// 4. Update UI...
updateUI(userData);
}

// GOOD: Specialized functions 👍
function validateUser(userData) { ... }
function saveUser(userData) { ... }
function notifyUser(email) { ... }
function updateUserUI(userData) { ... }

// “conductor” function
function processUser(userData) {
if (!validateUser(userData)) return;

saveUser(userData);
notifyUser(userData.email);
updateUserUI(userData);
}

This code is easier to test, debug, and reuse.

style.display vs classList (SoC best practice)

This is a perfect example of Separation of Concerns.

  • JavaScript (Logic) manages the state (e.g., “is the menu open?”).

  • CSS (Presentation) manages the appearance (e.g., “if the menu is open, show it”).

  • style.display (Not optimal approach): element.style.display = "block"; Analogy: JS overrides CSS and hand-paints the element. It mixes responsibilities. It’s hard to add an animation (you’d have to do it in JS) and hard to override.

  • classList (Better approach): element.classList.add("is-visible"); Analogy: JS sticks a label (.is-visible) on the element. CSS, in a separate file, sees that label and decides what to do.

    /* CSS */
    .menu { display: none; opacity: 0; }
    .menu.is-visible { display: block; opacity: 1; transition: opacity 0.3s; }

    Now you can change the animation or the look by editing only CSS, without ever touching JavaScript.

innerHTML = vs innerHTML += (Performance)

  • innerHTML = "..." (Assignment): OK. What it does: Clears all old content and replaces it with the new one. It’s a single, efficient operation.

  • innerHTML += "..." (Concatenation): TERRIBLE PERFORMANCE ❌ What it does: To add an element:

    1. The browser reads all existing HTML and turns it into a string.
    2. It adds your new string chunk.
    3. Destroys all existing DOM nodes.
    4. Re-parses and recreates all nodes from scratch (old + new). *Analogy (+=): To add a single book to a bookshelf, you completely empty all shelves, throw away the old books, and then put back copies of the old books plus the new one. Madness.
  • Solution (to add): Use createElement() and appendChild(). Analogy: You take the new book and put it on the shelf. Done. You don’t touch the others.

.className vs .classList (Best practice)

  • .className (The Blunt Weapon): It’s a string. If an element has class="old-class" and you do el.className = "new-class", you’ve deleted the old class.

  • .classList (The Surgical Kit): It’s a special object with precise methods. It’s the modern and safe way.

    el.classList.add("new");
    el.classList.remove("old");
    el.classList.toggle("active"); // Adds if missing, removes if present

    Rule: Always use classList.

.textContent vs innerHTML (Security)

Analogy: textContent is a marker (safe). innerHTML is a Harry Potter magic pen (powerful but dangerous).

  • .textContent (The Safe Choice ✅)

    • Inserts only plain text.
    • If a user types <script>alert('hacked!')</script> in their name, textContent treats it as harmless text and literally shows the string <script>... on the page.
    • Use this by default for any data coming from a user.
  • .innerHTML (The Dangerous Choice ⚠️)

    • Interprets and executes any HTML tags in the string.
    • If a user types <script>... and you insert it with innerHTML, the script will run. This is the web’s #1 security hole (Cross-Site Scripting - XSS).
    • Rule: Use innerHTML only if 1) you wrote the HTML yourself or 2) the source is 100% safe and trusted.




37. Immutability and Style

Immutability (General Concept)

This is a fundamental pattern for writing predictable code. Analogy: The difference between modifying an original master document (Mutation) and making a photocopy and modifying that (Immutability).

  • Mutation ❌ (The Evil): You modify an original array or object.

    function addUser(users) {
    users.push({ name: "New" }); // Mutates the original array!
    return users;
    }
    const myList = [{ name: "Mario" }];
    const newList = addUser(myList);
    // Problem: now 'myList' has changed! [ {name: "Mario"}, {name: "New"} ]
    // Any other part of the code that used 'myList' is now "broken"
    // or has unexpected data. This is called a "Side Effect".
  • Immutability ✅ (The Good): You create a copy with the changes and return the copy. The original stays intact.

    function addUser(users) {
    // Use the Spread Operator to make a photocopy
    const newList = [...users, { name: "New" }];
    return newList;
    }
    const myList = [{ name: "Mario" }];
    const newList = addUser(myList);
    // 'myList' is still [ {name: "Mario"} ] (intact!)
    // 'newList' is [ {name: "Mario"}, {name: "New"} ]

    This code is predictable, safe, and easier to debug.

.sort() (Destructive) vs .toSorted() (Immutable)

This is the perfect example of Immutability.

  • .sort() ❌: It’s a destructive method. It modifies (mutates) the original array.
  • .toSorted() ✅: It’s a modern, immutable method. It returns a new sorted copy, leaving the original intact.
  • (The same applies to reverse() vs toReversed() and splice() vs toSpliced()).

Style: Readability vs Conciseness (One-liner vs Multi-line)

Analogy: A "one-liner" (code on one line) is like trying to be “clever” and talk in a super-compact way. Multi-line code “tells a clear story”.

  • One-liner (Concise but hard to debug): const average = array.map(n => n * 2).filter(n => n > 10).reduce((a, b) => a + b, 0); Where do you put console.log to see intermediate results? You can’t.

  • Multi-line (Readable and easy to debug):

    const mapped = array.map(n => n * 2);
    // Easy to debug!
    console.log("After map:", mapped);

    const filtered = mapped.filter(n => n > 10);
    console.log("After filter:", filtered);

    const average = filtered.reduce((a, b) => a + b, 0);

Verdict: Readability and ease of debugging almost always beat the “cleverness” of conciseness. Write code that even a sleepy “you” 6 months from now can understand.

Swap Algorithm

How to swap the values of two variables.

  • Classic (temp) (The “Three-Seat Carousel”): Analogy: You need to swap two people (A and B) on two chairs, but they can’t stand up at the same time. You need a temporary chair (temp).

    1. A moves to temp.
    2. B moves to A’s chair.
    3. A (who was on temp) moves to B’s chair.
    const temp = a;
    a = b;
    b = temp;
  • Destructuring (Modern, ES6): Analogy: Magic. The two people swap places instantly.

    [a, b] = [b, a];

    This is cleaner, more concise, and does the exact same thing.





38. Code Evolution

Your code is never born perfect. It evolves. Understanding these steps helps you write better code from the start.

  • From Hardcoded to Dynamic:

    • Before: console.log("Welcome Mario!");
    • After: const name = prompt("What’s your name?"); console.log(`Welcome ${name}\!`); Code stops having “carved in stone” values and starts using variables.
  • From Repetitive to DRY (Don't Repeat Yourself):

    • Before: You copy and paste the same 10-line block in three different places.
    • After: You create one single function with those 10 lines and call it in three different places.
    • Benefit: If you need to change something, you change it in one place.
  • From Procedural to Event-Driven:

    • Before (Procedural): Code runs everything in order, top to bottom, once, and then ends.
    • After (Event-Driven): Code loads its functions and then... waits. It does nothing until the user does something (e.g. addEventListener("click", ...)). This is the model for almost everything on the web.
  • From Global to Modular:

    • Before (Global): All your variables (score, lives, userName) are in the “public square” (Global Scope), where anyone can touch them and break them.
    • After (Modular): You group related variables into “houses” (Objects) or “factories” (Classes) that protect them.
    // From this:
    let score = 0;
    let lives = 3;
    function increaseScore() { ... }

    // To this:
    const Game = {
    score: 0,
    lives: 3,
    increaseScore() { ... },
    loseLife() { ... }
    };

    This protects your data and makes the code infinitely more organized.













Advanced Patterns and Algorithms

39. Regex - The Language of Patterns

Regular Expressions (Regex or RegExp) are like a super sophisticated metal detector for text. While a normal metal detector only finds "metal", a regex can be programmed to find any pattern you can describe: email addresses, phone numbers, dates, tax IDs, duplicate words, or even complex patterns like "all words that start with 'A' and end with 'o'".

Imagine having to find all phone numbers in a 1000-page document: manually it would take you days, a regex does it in milliseconds.

Anatomy of a Regex

A regex is enclosed between two slashes /pattern/, like a math formula in parentheses. But these slashes are more than simple delimiters: they are the boundary between the normal world of JavaScript and the magical world of patterns.

// 1. Literal Regex (more common and performant)
// Think of this like /hello/
const regex = /hello/i; // Searches "hello", ignoring uppercase/lowercase

// 2. RegExp Constructor (used when the pattern is dynamic)
const wordToSearch = "world";
const dynamicRegex = new RegExp(wordToSearch, "i"); // Searches "world"

// How do you use it?
const text = "Hello World, how are you?";

// .test() - The Metal Detector (Yes/No)
// It only answers: "Is it there or not?" Returns true or false.
console.log(regex.test(text)); // true
console.log(dynamicRegex.test(text)); // true

// .match() - The Extractor (What did you find?)
// String.prototype.match() gives you the results.
console.log(text.match(regex)); // ["Hello"]
console.log(text.match(dynamicRegex)); // ["World"]

Flags - Global Modifiers

Flags are like switches you flip on your metal detector. They go after the final slash and change the global behavior of the search.

  • g (global): The "Find All" switch. Without g, the regex stops at the first match it finds. With g, it keeps searching until the end of the string, returning all matches.

    "hello hello".match(/hello/);  // ["hello"] (stops at the first)
    "hello hello".match(/hello/g); // ["hello", "hello"] (finds all)
  • i (case-insensitive): The "Ignore Uppercase/Lowercase" switch. Treats "A" and "a" as if they were the same character.

    /javascript/.test("JavaScript"); // false
    /javascript/i.test("JavaScript"); // true
  • m (multiline): The "Multi-Line" switch. By default, the special characters ^ (start) and $ (end) only work on the whole string. With m, ^ and $ match the start and end of each individual line (separated by \n).

  • s (dotAll): The "Dot Is Everything" switch. By default, . (dot) matches any character except the "newline" (\n). With s, . matches literally everything, including the "newline".

Combining flags: You can stick them all together, in any order. /pattern/gi (Global + Case-Insensitive)


Special Characters - Regex Superpowers

Some characters in regex have special meanings, like magical symbols in a spell. These are your main tools:

  • . (The Wildcard): The dot matches any single character (letter, number, space, symbol), except the "newline" (unless you use the s flag).

    /h.llo/.test("hello");  // true
    /h.llo/.test("h9llo"); // true
    /h.llo/.test("h llo"); // true
    /h.llo/.test("hllo"); // false (missing a character)
  • ^ (The Start Anchor): Matches the start of the string. Analog: Says "the string must start with this".

    /^Hello/.test("Hello world");  // true
    /^Hello/.test("Hey, Hello"); // false (doesn’t start with Hello)
  • $ (The End Anchor): Matches the end of the string. Analog: Says "the string must end with this".

    /world$/.test("Hello world"); // true
    /world$/.test("world hello"); // false (doesn’t end with world)
    // Combined: /^Hello$/ tests EXACTLY "Hello"
  • | (Alternative / OR): The "pipe" symbol means "or". Analog: It’s a fork in the road. "Take this road OR the other one".

    /dog|cat/.test("I like the dog");  // true
    /dog|cat/.test("I like the cat"); // true
    /dog|cat/.test("I like the mouse"); // false

Character Classes [] - The Exclusive Club

Square brackets create a "club" of characters. The pattern matches if it finds ANY ONE of the club members in that position.

  • Character Set (The Club): /[aeiou]/ matches a single vowel.

    /c[aeiou]t/.test("cat"); // true ('a' is in the club)
    /c[aeiou]t/.test("cot"); // true ('o' is in the club)
    /c[aeiou]t/.test("c9t"); // false ('9' is not in the club)

    Useful for "leetspeak": m[o0]n[e3]y matches "money", "m0ney", "m0n3y", etc.

  • Range (The Hyphen): Instead of writing [0123456789], you use a hyphen. /[a-z]/ // Any lowercase letter /[A-Z]/ // Any uppercase letter /[0-9]/ // Any digit /[a-zA-Z0-9_]/ // Alphanumeric plus underscore (identical to \w)

  • Negation (The Bouncer ^): If the first character inside [] is ^, it means "match any character EXCEPT those in this club". /[^aeiou]/ // Matches any consonant (or number, or space...) /[^0-9]/ // Matches anything that is not a digit


Predefined Classes - Shortcuts

For the most common "clubs", JavaScript gives you shortcuts (or "macros"):

  • \d (Digit): Any digit. Equivalent to: [0-9]

  • \D (Non-Digit): Anything that is not a digit. Equivalent to: [^0-9]

  • \w (Word Character): Any alphanumeric character (A-Z, a-z, 0-9) plus underscore (_). Equivalent to: [A-Za-z0-9_] Warning: It does not include the hyphen -!

  • \W (Non-Word Character): Anything that is not a \w (spaces, punctuation, symbols).

  • \s (Space): Any whitespace character (space, tab \t, "newline" \n).

  • \S (Non-Space): Anything that is not whitespace.

  • \b (Word Boundary): This one is special. It’s a zero-width "anchor". It matches the position between a \w and a \W (i.e., the boundary of a word). Analog: It’s like looking for the "edge of the sidewalk" of a word.

    /\bcat\b/.test("the cat sat"); // true ('cat' is a whole word)
    /\bcat\b/.test("category"); // false ('cat' is *inside* a word)

Quantifiers - How Many Times?

Quantifiers specify how many times the element immediately before must repeat.

  • ? (Zero or One): Makes the element optional. Analog: "Color or Colour? Doesn’t matter".

    /colou?r/.test("color");  // true (0 'u')
    /colou?r/.test("colour"); // true (1 'u')
  • + (One or More): The element must appear at least once. Analog: "I want a number!"

    /\d+/.test("12345"); // true (there are 5 digits)
    /\d+/.test("abc"); // false (there isn’t *at least one* digit)
  • * (Zero or More): The element may or may not be there, even many times. Analog: "Spaces? Maybe yes, maybe no, maybe many".

    /ab*c/.test("ac");     // true (0 'b')
    /ab*c/.test("abbbc"); // true (3 'b')
    /\s*/.test(""); // true (0 spaces)
  • {n} (Exactly n times): /\d{4}/ // Matches exactly 4 digits (e.g., a PIN)

  • {n,m} (From n to m times): /\d{2,4}/ // Matches from 2 to 4 digits

  • {n,} (At least n times): /\d{3,}/ // Matches at least 3 digits


Escape \ - When Special Becomes Normal

The backslash \ is your "special-powers disabler". If you want to search literally for a character that has a special meaning (like ., +, *, ?, $), you need to "escape" it by putting a \ in front of it.

// WRONG: I want to search for "10.00"
/10.00/.test("price 10.00"); // true
/10.00/.test("price 10X00"); // true! (Because '.' is a wildcard)

// RIGHT: Escape the dot
/10\.00/.test("price 10.00"); // true
/10\.00/.test("price 10X00"); // false

// Other examples:
// To search for a literal "+": \+
// To search for a literal "$": \$
// To search for a literal "\": \\ (double escape)

Anchors (^, $) vs Space (\s) (Whole-word solution)

  • The problem: You want to find word but only if it’s a whole word. /\sword\s/ (with spaces) is a trap!

    • Matches: " in word here " (OK)
    • Does not match: "word" (at the start/end, it doesn’t have spaces around it)
  • Anchors (^, $): As seen, ^ and $ are "position assertions" (zero-width). They verify a position (start/end of string), they don’t consume a character.

  • The solution (combined): To match a word surrounded by spaces OR at the boundaries of the text, you must use an OR group. And to avoid "capturing" (see below) those spaces, we use a non-capturing group (?:...).

    /(\s|^)word(\s|$)/ (Simple version with capture) /(?:\s|^)word(?:\s|$)/ (Optimized version)

    • (?:\s|^) = "match a space OR the start of the string"
    • (?:\s|$) = "match a space OR the end of the string" This pattern matches word in all these cases: "word" (OK) "word here" (OK) "see word" (OK) "see word here" (OK)

Groups: Capturing () vs Non-Capturing (?:...)

Parentheses () are fundamental, but they do two things at the same time:

  1. Group: They allow you to apply a quantifier (like ?, +) to a group of characters.

    • abc? (only the c is optional)
    • (abc)? (the whole group "abc" is optional)
  2. Capture: They store the piece of string that matched that group.

Analog: The Bus 🚌

  • () (Capturing Group): It’s a bus. It groups the students (keeps them together) AND the teacher takes a photo 📸 of that specific group for the yearbook (it captures it).

    const text = "user@example.com";
    const match = text.match(/(\w+)@(\w+\.\w+)/);
    // match[0] = "user@example.com" (full match)
    // match[1] = "user" (Photo 📸 of Group 1)
    // match[2] = "example.com" (Photo 📸 of Group 2)
  • (?:...) (Non-Capturing Group): Sometimes you only want to group, but you don’t care about the photo (you don’t want to store that piece). The (?:...) syntax does exactly that. Analog: It’s a bus (groups) but the teacher doesn’t take the photo (doesn’t capture).

    // I only want to know if it starts with "http" or "https",
    // but I don’t care *which* one it is.
    const regex = /(?:http|https):\/\//;
    const match = "https://google.com".match(regex);
    // match[0] = "https://"
    // match[1] = undefined (No photo 📸!)
  • Why use it?

    1. Performance: It doesn’t waste memory saving "photos" you won’t use.
    2. Clarity: It keeps the results array clean, containing only the groups you wanted to extract. Rule: Use () only if you need to extract that piece. If you only need to group (for a | or a ?), use (?:...).

Lookahead and Lookbehind - Eyes of the Future

These are advanced patterns that "look" forward or backward without consuming characters. They match a condition at a position.

  • (?=...) (Positive Lookahead): "Find X, only if it’s followed by Y". Analog: "Find the number, only if you see the € symbol after."

    // Extracts only the number from a price
    /\d+(?=)/.exec("costs 50€")[0]; // "50"
    // "€" is the condition, but it’s not part of the match.
  • (?!...) (Negative Lookahead): "Find X, only if it is NOT followed by Y".

  • (?<=...) (Positive Lookbehind): "Find X, only if it’s preceded by Y". Analog: "Find the number, only if you see the $ symbol before."

    /(?<=)\d+/.exec("costs €50")[0]; // "50"
  • (?<!...) (Negative Lookbehind): "Find X, only if it is NOT preceded by Y".


Real-World Patterns - Useful Regex

Here are some patterns you’ll often end up using:

// EMAIL (simplified but effective)
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

// URL (simplified)
const urlRegex = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(\S*)$/i;

// ITALIAN DATE (DD/MM/YYYY)
const dateRegex = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;

// EXTRACT NUMBERS FROM TEXT
const text = "I have 3 apples and 2 pears for €5.50";
const numbers = text.match(/\d+(\.\d+)?/g); // ["3", "2", "5.50"]

// REMOVE HTML TAGS (simplified)
const textWithoutHTML = htmlString.replace(/<[^>]*>/g, '');

// CAPITALIZE FIRST LETTERS (uses a function in replace!)
const capitalize = str => str.replace(/\b\w/g, (letter) => letter.toUpperCase());
capitalize("hello world"); // "Hello World"




40. Call Stack and Recursion - The Tower of Plates

The Call Stack (or "Call Stack") is JavaScript’s notebook. It’s its short-term "brain", where it keeps track of which function it is executing at this exact moment and which ones are "paused", waiting to be completed.

The Mental Model: The Stack of Plates (LIFO)

The perfect analogy is a stack of dirty dishes next to the sink.

  1. You have a dirty plate (you call functionA), you put it on the stack. [functionA]
  2. Another plate arrives (functionA calls functionB), you put it on top of the first. [functionA, functionB]
  3. A third plate arrives (functionB calls functionC), you put it on the very top. [functionA, functionB, functionC]
  4. Now you have to wash. Which one do you wash? The last one you put on top (functionC).
  5. Done washing C, you remove it from the stack (pop). [functionA, functionB]
  6. Now you wash B, remove it from the stack. [functionA]
  7. Finally, you wash A (the first one you put in) and the stack is empty. []

This is called LIFO (Last In, First Out): the last plate added is the first to be washed. The Call Stack works exactly like this.

Let’s see it in code:

function first() {
console.log("1. Start first");
second();
console.log("5. End first");
}

function second() {
console.log("2. Start second");
third();
console.log("4. End second");
}

function third() {
console.log("3. Execute third");
}

// 1. Start execution
first();

// Output:
// 1. Start first
// 2. Start second
// 3. Execute third
// 4. End second
// 5. End first

// The Call Stack evolved like this:
// [] - (Empty)
// [first] - (Enter 'first')
// [first, second] - ('first' calls 'second')
// [first, second, third] - ('second' calls 'third')
// [first, second] - ('third' ends, is removed)
// [first] - ('second' ends, is removed)
// [] - ('first' ends, the stack is empty)

The Famous Error: Stack Overflow What happens if you keep putting plates on the stack, forever, without ever washing? The tower collapses. This is a Stack Overflow: you called too many functions (often a function that calls itself forever) without ever letting them finish, filling JavaScript’s "notebook" until it blows up.


Recursion - The Function That Calls Itself

Recursion is when a function solves a problem by calling itself with a "smaller" version of the problem.

Analog: Think of Russian nesting dolls 🪆. To open the doll (solve the problem), you open it and find... a smaller doll (a smaller version of the problem). You keep opening them until you find the last, tiny solid doll (the "base case").

A recursive function has two mandatory parts:

  1. Base Case (The Solid Doll): The stop condition. It’s the simplest version of the problem that can be solved without another call. Without this, you’ll get a Stack Overflow (the infinite stack of plates).
  2. Recursive Case (The Middle Dolls): The point where the function "breaks" the problem into a smaller piece and calls itself to solve it.

Example: Factorial (!)

The factorial of n (written n!) is n * (n-1) * (n-2) * ... * 1. E.g. 4! = 4 * 3 * 2 * 1 = 24.

  • Iterative Version (with a for loop):

    function factorialIterative(n) {
    let result = 1;
    for (let i = n; i > 1; i--) {
    result = result * i;
    }
    return result;
    }
  • Recursive Version (Elegant): The logic is: 4! = 4 * 3! ... and 3! = 3 * 2! ... and 2! = 2 * 1!.

    function factorial(n) {
    // 1. BASE CASE (The solid doll)
    if (n <= 1) {
    return 1;
    }

    // 2. RECURSIVE CASE (n * smaller version)
    return n * factorial(n - 1);
    }

    // Let’s trace factorial(4)
    //
    // STACK (Stack of plates):
    // [factorial(4)] -> must wait for factorial(3)
    // [f(4), f(3)] -> must wait for factorial(2)
    // [f(4), f(3), f(2)] -> must wait for factorial(1)
    // [f(4), f(3), f(2), f(1)] -> f(1) is the BASE CASE!
    //
    // Now the stack "resolves" (you wash plates from the top):
    // f(1) returns 1.
    // [f(4), f(3), f(2)] -> f(2) receives 1 and does return 2 * 1 = 2
    // [f(4), f(3)] -> f(3) receives 2 and does return 3 * 2 = 6
    // [f(4)] -> f(4) receives 6 and does return 4 * 6 = 24
    // [] -> Final result: 24

Example: Decimal to Binary Conversion

How do you convert a decimal number (e.g. 10) to binary (e.g. "1010")? The algorithm is:

  1. Divide the number by 2.
  2. Write down the remainder (it will be 0 or 1).
  3. Repeat the process with the quotient.
  4. Keep going until the quotient is 0 or 1.
  5. Read the remainders backwards.

Recursion is perfect for this, because the Call Stack "remembers" the remainders in the right order for us!

function decimalToBinary(num) {
// 1. BASE CASE (The solid doll)
if (num <= 1) {
return String(num); // Returns "1" or "0"
}

// 2. RECURSIVE CASE
const quotient = Math.floor(num / 2);
const remainder = num % 2;

// The magic: call the function on the quotient (smaller)
// and append the remainder *at the end*.
return decimalToBinary(quotient) + String(remainder);
}

// Let’s trace decimalToBinary(10):
//
// STACK:
// [d(10)] -> must wait for d(5). Remainder: 0
// [d(10), d(5)] -> must wait for d(2). Remainder: 1
// [d(10), d(5), d(2)] -> must wait for d(1). Remainder: 0
// [d(10), d(5), d(2), d(1)] -> d(1) is the BASE CASE!
//
// The stack "resolves":
// d(1) returns "1".
// [d(10), d(5), d(2)] -> d(2) receives "1" and does return "1" + "0" = "10"
// [d(10), d(5)] -> d(5) receives "10" and does return "10" + "1" = "101"
// [d(10)] -> d(10) receives "101" and does return "101" + "0" = "1010"
// [] -> Final result: "1010"

Recursion with Caching (Memoization)

The problem: pure recursion can be incredibly inefficient. Take the Fibonacci example (where fib(n) = fib(n-1) + fib(n-2)). To compute fib(5), you must compute:

  • fib(4) and fib(3)
  • For fib(4), you must compute fib(3) and fib(2) ... you’ve already computed fib(3) twice! For fib(40), you will compute fib(2) millions of times.

The solution (Memoization): Analog: It’s like writing the answer to a hard problem on a sticky note. The next time someone asks you the exact same question, you don’t recompute it from scratch. You just read the sticky note.

We use a "cache" (an object) to store results that have already been computed.

// SLOW version (Exponential)
function fibSlow(n) {
if (n <= 1) return n;
return fibSlow(n - 1) + fibSlow(n - 2);
}

// FAST version (Memoization)
// We use an IIFE (a self-invoking function)
// to create a private "cache" that the inner function can use.
const fibMemo = (function() {
const cache = {}; // Our private "notepad"

return function fib(n) {
// 1. Check the notepad (cache)
if (n in cache) {
return cache[n]; // Found! Read the sticky note.
}

// 2. Base case
if (n <= 1) {
return n;
}

// 3. Not found? Compute AND store
const result = fib(n - 1) + fib(n - 2);
cache[n] = result; // Write the result on the sticky note
return result;
};
})(); // The final () executes the outer function

console.time("Slow");
console.log(fibSlow(40)); // Takes seconds!
console.timeEnd("Slow");

console.time("Fast");
console.log(fibMemo(40)); // Instant!
console.timeEnd("Fast");

Recursion is an elegant concept, and memoization makes it a practical and powerful tool.





41. Practical Algorithms - The Recipes of Code

Algorithms are the heart of programming. They’re not code, they’re ideas. They’re the tested and proven "recipes" programmers have used for decades to solve common problems, like sorting a list or finding a piece of data. Learning these patterns is like a chef learning how to make béchamel sauce or a basic dough: they’re the fundamental building blocks for creating complex dishes (programs).

Decimal → Binary Conversion Algorithm

We’ve already seen the recursive version of this algorithm (in Section 19 on the Call Stack), which is elegant and uses the stack to "remember" remainders.

There is also an iterative version (with a while loop), which is often more performant and doesn’t risk a "Stack Overflow" with huge numbers. It’s the "manual" implementation of the same concept.

Analog: Instead of using nesting dolls (recursion), you use a notepad (binary) and an abacus (input).

The algorithm is: "Divide by 2, write down the remainder, repeat with the quotient."

function decimalToBinary(input) {
if (input === 0) return "0"; // Base case

let binary = ""; // Our "notepad" (string)
let number = input; // Our "abacus"

// Keep going until the abacus is zero
while (number > 0) {
// 1. What is the remainder of division by 2?
const remainder = number % 2; // Will be 0 or 1

// 2. "Prepend" the remainder to the string
// (because remainders are read backwards)
binary = remainder + binary;

// 3. Prepare the next round with the quotient
number = Math.floor(number / 2);
}

return binary;
}

// Test
console.log(decimalToBinary(10)); // "1010"
console.log(decimalToBinary(255)); // "11111111"

Sorting Algorithms

Sorting a list is one of the most classic problems in computer science.

Bubble Sort - The Simplest (but Inefficient)

Analog: It’s like a jar of bubbles. The "lighter" bubbles (the smaller numbers) slowly "rise" toward the start of the list.

  • How it works: It scans the array, comparing each element (array[j]) with the next one (array[j+1]). If they’re in the wrong order, it swaps them. It repeats this whole process again and again until the array is sorted.
  • Performance: It’s terribly slow ($O(n^2)$). If the array doubles, runtime quadruples. Never use it in production, but it’s great for learning swaps.
function bubbleSort(arr) {
const array = [...arr]; // Copy so we don’t modify the original
const n = array.length;

for (let i = 0; i < n - 1; i++) {
let swapped = false; // Optimization

for (let j = 0; j < n - i - 1; j++) {
// Compare adjacent elements
if (array[j] > array[j + 1]) {
// Swap with destructuring
[array[j], array[j + 1]] = [array[j + 1], array[j]];
swapped = true;
}
}

// Optimization: if a whole pass made no swaps,
// the array is already sorted. Exit early.
if (!swapped) break;
}

return array;
}

Quick Sort - Fast and Elegant (Divide and Conquer)

Analog: It’s like organizing a library.

  1. Pick a random book (pivot).
  2. Split all the other books into two piles: left (those that come before the pivot alphabetically) and right (those that come after).
  3. Hand the two smaller piles to two assistants, telling them: "Do the exact same thing I did" (Recursion!).
  4. When they give you the sorted piles back, you merge them: [sortedLeftPile, pivot, sortedRightPile].
  • Performance: It’s one of the fastest algorithms on average ($O(n \log n)$). It relies heavily on recursion (and therefore on the Call Stack!).
function quickSort(arr) {
// Base case: an array with 0 or 1 element is already sorted
if (arr.length <= 1) return arr;

// 1. Choose a pivot (we take the last one)
const pivot = arr[arr.length - 1];

// 2. Split into two piles (left/right)
const left = [];
const right = [];

for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}

// 3. & 4. Recurse and merge
return [...quickSort(left), pivot, ...quickSort(right)];
}

Search Algorithms

Binary Search - The Dictionary

This is an incredibly fast algorithm for finding an element, but it has a fundamental prerequisite: the array MUST already be sorted.

Analog: It’s how you look up a word in a dictionary.

  1. Open the dictionary exactly in the middle.
  2. Is the word you see (mid) the one you’re looking for? Great, you’re done.
  3. Does the word you’re looking for come after (it’s larger)? Then you know it’s useless to look at the first half of the dictionary. You mentally throw away the entire left half.
  4. Does the word you’re looking for come before (it’s smaller)? You throw away the entire right half.
  5. Repeat the process (open in the middle, compare, throw away half) on what remains.
  • Performance: It’s extremely fast ($O(\log n)$). To find 1 element in a billion, it takes at most 30 checks (while a for loop would take about 500 million checks on average).
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;

while (left <= right) {
// 1. Find the middle index
const mid = Math.floor((left + right) / 2);

// 2. Check if it’s the one
if (arr[mid] === target) {
return mid; // Found! Return the index
}

// 3. Is it larger? Throw away the left half
if (arr[mid] < target) {
left = mid + 1;
}
// 4. Is it smaller? Throw away the right half
else {
right = mid - 1;
}
}

return -1; // Not found
}

const sorted = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
console.log(binarySearch(sorted, 7)); // 3 (index)
console.log(binarySearch(sorted, 6)); // -1 (not found)

String Algorithms

Palindrome - Check if a String Is a Palindrome

Definition: A string that reads the same both ways (e.g., "anna", "i topi non avevano topi"). Challenge: You must ignore uppercase/lowercase, spaces, and punctuation.

function isPalindrome(str) {
// 1. Clean the string
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');

// 2. "Lazy" method: compare with its reverse
const reversed = cleaned.split('').reverse().join('');
return cleaned === reversed;
}

// "Two Pointers" method (more performant)
function isPalindromeTwoPointers(str) {
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
let left = 0;
let right = cleaned.length - 1;

while (left < right) {
if (cleaned[left] !== cleaned[right]) {
return false; // They don’t match
}
left++;
right--;
}

return true; // They reached the center
}

console.log(isPalindrome("A man, a plan, a canal: Panama")); // true

Anagrams - Check if Two Strings Are Anagrams

Definition: Two strings that use exactly the same letters, but in a different order (e.g., "listen", "silent").

// Method 1: The Sorting Trick
function areAnagramsSort(str1, str2) {
// Helper function to clean and sort
const cleanSort = s => s.toLowerCase()
.replace(/[^a-z]/g, '')
.split('')
.sort()
.join('');

return cleanSort(str1) === cleanSort(str2);
}

// Method 2: Frequency Map (See Part I, the final section of Chapter 6)
function areAnagramsMap(str1, str2) {
const s1 = str1.toLowerCase().replace(/[^a-z]/g, '');
const s2 = str2.toLowerCase().replace(/[^a-z]/g, '');

if (s1.length !== s2.length) return false;

const count = {};

// Count letters in the first string
for (const char of s1) {
count[char] = (count[char] || 0) + 1;
}

// Subtract letters in the second string
for (const char of s2) {
if (!count[char]) return false; // Extra letter
count[char]--;
}

return true; // If all counts are 0, it’s an anagram
}

console.log(areAnagramsSort("listen", "silent")); // true

Numeric Algorithms

Prime Numbers - Checking and Generating

Definition: A number greater than 1, divisible only by 1 and itself.

// Check if ONE number is prime (optimized version)
function isPrime(n) {
if (n <= 1) return false;
if (n <= 3) return true;

// Optimization: immediately exclude multiples of 2 and 3
if (n % 2 === 0 || n % 3 === 0) return false;

// Optimization: check only up to the square root
for (let i = 5; i * i <= n; i += 6) {
// Check i=5 and i+2=7, then i=11 and i+2=13, etc.
if (n % i === 0 || n % (i + 2) === 0) {
return false;
}
}

return true;
}

// Sieve of Eratosthenes - Generate ALL primes up to 'max'
function sieveOfEratosthenes(max) {
// 1. Create an array of "yes" (true)
const prime = new Array(max + 1).fill(true);
prime[0] = prime[1] = false; // 0 and 1 are not prime

for (let i = 2; i * i <= max; i++) {
// 2. If 'i' is still "yes" (it’s prime)...
if (prime[i]) {
// 3. ...then "cross out" all its multiples
for (let j = i * i; j <= max; j += i) {
prime[j] = false;
}
}
}

// 4. Collect the results
const primes = [];
prime.forEach((isPrime, number) => {
if (isPrime) primes.push(number);
});

return primes;
}

Fibonacci - The Golden Sequence

Definition: Each number is the sum of the previous two (0, 1, 1, 2, 3, 5, 8...). The recursive version with memoization is great (seen in Section 19). The iterative version (with a loop) is the most efficient overall.

function fibonacciIterative(n) {
if (n <= 1) return n;

let prev = 0;
let curr = 1;

for (let i = 2; i <= n; i++) {
// The magic of swap with destructuring:
// The new 'prev' becomes the 'curr'
// The new 'curr' becomes (old prev + old curr)
[prev, curr] = [curr, prev + curr];
}

return curr;
}

console.log(fibonacciIterative(7)); // 13

GCD/LCM (Euclidean Algorithm)

  • GCD (Greatest Common Divisor): The largest number that divides both.
  • LCM (Least Common Multiple): The smallest number that is a multiple of both.

The Euclidean Algorithm for GCD is one of the oldest and fastest algorithms. Analog: gcd(a, b) is a if b is 0. Otherwise, it’s gcd(b, a % b).

// Euclidean Algorithm (Recursive)
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b);
}

// Formula for LCM
function lcm(a, b) {
// (a * b) can be huge, better to divide first
return (a / gcd(a, b)) * b;
}

console.log(gcd(48, 18)); // 6
console.log(lcm(21, 6)); // 42




42. Input Sanitization and Validation

Input validation is your first line of defense against bugs, corrupted data, and security vulnerabilities. It’s like the security check at the airport: you can’t (and shouldn’t) trust what the user gives you. You must check rigorously before letting the data "board" your system.

  • Validation: It’s the checking process. It answers: "Is this data in the format I expect?" (e.g., "Is it a number? Is it a valid email?"). The action is accept or reject.
  • Sanitization: It’s the cleaning process. It answers: "How can I make this data safe?" (e.g., "Remove <script> tags"). The action is modify and clean.

Robust Validation with the Guard Pattern

The "Guard Clause" (or "Return Early") pattern is the cleanest way to write validation functions.

Analog: It’s like a bouncer at a party. Instead of letting everyone in and then looking for the ones who don’t belong (nested ifs), the bouncer checks IDs at the entrance. If you don’t have a ticket, they bounce you (return) immediately. Only those who meet all requirements get into the party (the main logic).

BAD: The "Pyramid of Doom" 👎 This code is hard to read. The "happy path" (the one that does the real work) is buried at the bottom, inside three levels of if.

function processData(data) {
if (data) {
if (data.isValid) {
if (data.value > 0) {
// ...finally, the code we care about...
// ...buried in here...
return data.value * 2;
} else {
return null; // Error case 3
}
} else {
return null; // Error case 2
}
} else {
return null; // Error case 1
}
}

GOOD: "Guard Clauses" pattern 👍 The code is "flat", readable, and the main logic is the last thing, not the most nested.

function processData(data) {
// Guard 1: Does the data exist?
if (!data) {
return null; // Exit immediately
}

// Guard 2: Is the data valid?
if (!data.isValid) {
return null; // Exit immediately
}

// Guard 3: Is the value positive?
if (data.value <= 0) {
return null; // Exit immediately
}

// If we got here, all guards let us through.
// The "happy path" is flat and easy to read.
return data.value * 2;
}

Validating Numeric Input

When you receive a number from an <input>, remember it’s always a string! You must validate it rigorously. Here’s a robust function that uses Guard Clauses:

function validateNumber(input, options = {}) {
// Set default options
const {
min = -Infinity,
max = Infinity,
integer = false, // Must it be an integer?
positive = false // Must it be > 0?
} = options;

// Guard 1: Is it required?
if (input === "" || input === null || input === undefined) {
return { valid: false, error: "Input required" };
}

// Convert
const num = Number(input);

// Guard 2: Is it a number? (Use Number.isNaN for safety)
if (Number.isNaN(num)) {
return { valid: false, error: "Must be a number" };
}

// Guard 3: Is it an integer?
if (integer && !Number.isInteger(num)) {
return { valid: false, error: "Must be an integer" };
}

// Guard 4: Is it positive?
if (positive && num <= 0) {
return { valid: false, error: "Must be a positive number" };
}

// Guard 5: Does it meet the minimum?
if (num < min) {
return { valid: false, error: `The number must be at least ${min}` };
}

// Guard 6: Does it meet the maximum?
if (num > max) {
return { valid: false, error: `The number must not exceed ${max}` };
}

// If it got here, it’s valid!
return { valid: true, value: num };
}

// --- Usage example ---
const userInput = "25";
const result = validateNumber(userInput, {
min: 1,
max: 100,
integer: true
});

if (!result.valid) {
alert(result.error);
} else {
console.log("Valid number:", result.value); // result.value is 25 (a number!)
}

Removing Special Characters and Sanitization

Sanitization is the process of cleaning data. You don’t reject it, you modify it to make it safe. Analog: It’s like filtering water before drinking it. You remove the dirt (dangerous characters) and keep the water (safe content).

The main tool is String.prototype.replace() with a Regex.

// Example 1: Basic sanitization (removes everything except letters, numbers, spaces)
function sanitizeBasic(str) {
// Regex: [^a-zA-Z0-9\s] -> "find everything that is NOT (^)
// a letter (a-z, A-Z), a number (0-9), or a space (\s)"
// ...and replace it with an empty string (delete it).
return str.replace(/[^a-zA-Z0-9\s]/g, '');
}

console.log(sanitizeBasic("Hello! This is a 100% test?"));
// Output: "Hello This is a 100 test"

// Example 2: Sanitizer for different contexts
const Sanitizer = {

// Cleans to create an ID or "slug" (e.g., for a URL)
forId(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Only letters, numbers, spaces, hyphens
.replace(/\s+/g, '-') // Spaces -> hyphens
.replace(/-+/g, '-') // Multiple hyphens -> one
.replace(/^-|-$/g, ''); // Remove hyphens at start/end
},

// The most important one: Sanitization for HTML (Prevent XSS)
// Don’t use regex! It’s too easy to get wrong.
// Use the 'textContent' trick!
forHTML(str) {
const div = document.createElement('div');
// By setting textContent, the browser "kills"
// any HTML tags (e.g., <script>) and treats it as text.
div.textContent = str;

// Reading innerHTML back gives you the "escaped" safe version.
// <script> becomes &lt;script&gt;
return div.innerHTML;
}
};

console.log(Sanitizer.forId(" The last coffee -- at €2.50! "));
// Output: "the-last-coffee-at-250"

Email Validation and Common Patterns

For complex validation, don’t reinvent the wheel. Use well-tested Regex patterns.

const Validator = {

patterns: {
// This regex is the practical "standard" (RFC 5322).
// Don’t try to write it yourself!
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,

// Example password (min 8, 1 uppercase, 1 lowercase, 1 number)
passwordStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/,

// Italian date
dateIT: /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/
},

// Simple validation function (Yes/No)
validate(type, value) {
if (!this.patterns[type]) {
console.error(`Validator "${type}" not found.`);
return false;
}
return this.patterns[type].test(value);
},

// Validation function with feedback (for UI)
validatePassword(password) {
const errors = []; // An error accumulator!

if (password.length < 8) {
errors.push("Must contain at least 8 characters");
}
if (!/[A-Z]/.test(password)) {
errors.push("Must contain at least one uppercase letter");
}
if (!/[a-z]/.test(password)) {
errors.push("Must contain at least one lowercase letter");
}
if (!/\d/.test(password)) {
errors.push("Must contain at least one number");
}

return {
valid: errors.length === 0,
errors: errors // Returns the list of issues
};
}
};

// --- Usage example ---
console.log(Validator.validate('email', 'test@test.com')); // true
console.log(Validator.validate('email', 'test.com')); // false

const passwordFeedback = Validator.validatePassword("pass");
console.log(passwordFeedback.valid); // false
console.log(passwordFeedback.errors);
// ["Must contain at least 8 characters", "Must contain at least one uppercase letter", ...]




43. State Management Patterns

State Management is like conducting an orchestra. The "State" is the musical score (the data: who is logged in, the score, the items in the cart). If every musician (UI component) has their own slightly different version of the score (duplicated or unsynced data), the result will be chaos.

These patterns exist to ensure everyone is playing from the exact same sheet music.

The "Single Source of Truth" (SSOT) Pattern

This is the most important principle: your application’s state must live in one single place.

Analog: Instead of having scattered notes on sticky notes all over the office (a userName in the header, another userName in the profile, a taskList here and another there), you have a single central "ledger" (or a main whiteboard) that everyone references.

The problem (without SSOT):

  1. A Header component has a variable userName = "Mario".
  2. A ProfilePage component has another variable userName = "Mario".
  3. The user updates their name to "Luigi" in the ProfilePage.
  4. ProfilePage updates its variable.
  5. RESULT (BUG): ProfilePage now says "Luigi", but the Header still says "Mario". The data is not synchronized.

The solution (with SSOT): There is a single StateManager object (the "ledger").

// A centralized "ledger" (Single Source of Truth)
const StateManager = {
// 1. Private state (the real ledger)
_state: {
user: { name: "Mario" },
tasks: [],
settings: { theme: 'light' }
},

// 2. A "gate" to READ (always returns a copy)
getState() {
// Return a copy to prevent accidental changes
return JSON.parse(JSON.stringify(this._state));
},

// 3. A "gate" to WRITE (the only way to change)
setState(path, value) {
// E.g. path = "user.name", value = "Luigi"
const keys = path.split('.');
let target = this._state;

// Walk the object to find where to write
for (let i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
}

target[keys[keys.length - 1]] = value;

// 4. Notify everyone that something changed!
this._notify(path, value);
},

// Notification system (see "Event-Driven")
_listeners: [],
subscribe(callback) {
this._listeners.push(callback);
},
_notify(path, value) {
this._listeners.forEach(cb => cb(path, value));
}
};
  • How it works now:

    1. Header and ProfilePage both read from StateManager.getState().
    2. The user changes their name. ProfilePage calls StateManager.setState("user.name", "Luigi").
    3. StateManager updates its state and _notify() alerts all subscribers.
    4. Header, being a subscriber, receives the notification and updates its UI.
    5. RESULT: The whole app is perfectly synchronized.

CRUD Pattern for Lists

CRUD is an acronym that describes the four fundamental operations for managing any data collection (like a task list, a shopping cart, a user list).

  • Create
  • Read
  • Update
  • Delete

Analog: It’s like managing a library of books (tasks).

Creating a class (like a TaskManager) is the cleanest way to encapsulate this logic, combining state (SSOT) with the methods to manipulate it.

class TaskManager {
constructor() {
// SSOT: 'this.tasks' is the single source of truth for tasks
this.tasks = [];
this.loadTasksFromStorage(); // Load saved data
}

// --- CREATE ---
// (Add a new book to the library)
addTask(text) {
const newTask = {
id: `task-${Date.now()}`, // Unique ID
text: text,
completed: false,
createdAt: new Date().toISOString()
};
this.tasks.push(newTask);
this.save(); // Save after every change
this.render(); // Update the UI
return newTask;
}

// --- READ ---
// (Find books in the library)
getTask(id) {
return this.tasks.find(task => task.id === id);
}

getAllTasks() {
return [...this.tasks]; // Return a *copy* (Immutability!)
}

getFilteredTasks(filter) { // E.g. filter = 'completed'
if (filter === 'completed') {
return this.tasks.filter(t => t.completed);
}
if (filter === 'pending') {
return this.tasks.filter(t => !t.completed);
}
return this.getAllTasks();
}

// --- UPDATE ---
// (Change a book’s cover or title)
updateTask(id, updates) { // 'updates' is an object, e.g. { text: "New text" }
const index = this.tasks.findIndex(task => task.id === id);
if (index === -1) return false; // Not found

// Merge the old task with the new changes
this.tasks[index] = {
...this.tasks[index], // Old data
...updates, // New data (overwrites)
updatedAt: new Date().toISOString()
};

this.save();
this.render();
return this.tasks[index];
}

// Helper method for a common update
toggleTask(id) {
const task = this.getTask(id);
if (task) {
this.updateTask(id, { completed: !task.completed });
}
}

// --- DELETE ---
// (Remove a book from the library)
deleteTask(id) {
const index = this.tasks.findIndex(task => task.id === id);
if (index === -1) return false;

this.tasks.splice(index, 1); // .splice() mutates the original array
this.save();
this.render();
return true;
}

// --- SUPPORT METHODS ---
save() {
// Use localStorage patterns (Part III, chapter 29)
localStorage.setItem('tasks', JSON.stringify(this.tasks));
}

loadTasksFromStorage() {
// Use the "Handle First Run" pattern
this.tasks = JSON.parse(localStorage.getItem('tasks')) || [];
}

render() {
// Logic to update the DOM (Part III)
console.log("Updating the UI with the new tasks...", this.tasks);
}
}

Reset Pattern and Initial State

How do you handle a "factory reset" of your application? (e.g., when a user logs out, or starts a "New Game").

The problem: You might be tempted to manually reset every piece of state.

// BAD: Easy to forget something 👎
function resetApp() {
StateManager.state.user = null;
StateManager.state.tasks = [];
StateManager.state.settings.theme = 'light';
// Oops! I forgot to reset state.ui.isLoading!
}

The solution (Pattern: Initial State as a Function): Define your initial state not as a static object, but as a function that returns a new object.

Analog: Instead of having a single original "registration form" that everyone scribbles on (an object), you have a stack of fresh, clean forms (a function). To reset, you throw away the scribbled form and grab a new one from the stack.

// 1. Define the initial-state "factory"
const createInitialState = () => ({
form: {
name: '',
email: '',
message: ''
},
ui: {
isSubmitting: false,
errors: [],
successMessage: null
},
data: []
});

// 2. Your manager uses the factory to start
const FormManager = {
state: createInitialState(), // Create the first "form"

// 3. Reset is now clean, safe, and complete!
reset() {
// Throw away the old state and grab a brand-new one
this.state = createInitialState();
this.render(); // Update the UI
},

// ...other methods...
render() {
console.log("Rendering state...", this.state);
}
};

// Usage:
FormManager.state.form.name = "Mario"; // Modify state
FormManager.reset(); // Reset!

Why a function and not a const object? If createInitialState were a const object, when you assign it to state (this.state = initialState), you’d be assigning a reference (a "link"). If you changed this.state.form.name, you’d also be changing the original initialState! The function guarantees you always get a fresh, clean copy.