JavaScript Real World Vademecum
Parte V: Pattern Avanzati e Algoritmi
La differenza tra il codice che funziona e il codice che scala risiede qui. Esploriamo algoritmi avanzati, Regex, design pattern e tecniche di ottimizzazione per uno sviluppo di livello professionale.
Pattern e Best Practices
Scrivere codice che funziona è il primo passo. Scrivere codice buono è l'obiettivo finale. Un "buon" codice è pulito, leggibile, facile da manutenere e difficile da rompere. Questa sezione raccoglie i "pattern" (modelli) e le "best practice" (abitudini) che trasformano un programmatore in un professionista.
34. Pattern di Sviluppo
Pattern di Accumulo
L'accumulo è uno dei pattern più comuni. È come riempire un secchio goccia a goccia. Inizi con un "contenitore" vuoto (che sia un numero, una stringa, o un array) e poi, dentro un ciclo, "accumuli" i risultati.
// 1. Accumulo Numerico (Somma)
let totale = 0; // Il secchio vuoto
const prezzi = [10, 20, 30];
for (const prezzo of prezzi) {
totale += prezzo; // Aggiungi ogni "goccia"
}
// totale ora è 60
// 2. Accumulo di Stringa (Costruzione)
let html = "<ul>"; // Il contenitore iniziale
const frutti = ["Mela", "Pera"];
for (const frutto of frutti) {
html += `<li>${frutto}</li>`; // Accumula pezzi di stringa
}
html += "</ul>";
// html ora è "<ul><li>Mela</li><li>Pera</li></ul>"
// 3. Accumulo in Array (Filtraggio)
const positivi = []; // L'array vuoto
const numeri = [-1, 10, -5, 20];
for (const num of numeri) {
if (num > 0) {
positivi.push(num); // Accumula solo i positivi
}
}
// positivi ora è [10, 20]
Il metodo .reduce() (visto nella Parte I, capitolo 5) è la versione funzionale e compatta del pattern di accumulo.
Flag Booleane - Gli Interruttori
Una "flag" (bandiera) è un interruttore della luce. È una variabile booleana (true/false) che usi per ricordare uno stato e controllare il flusso del programma.
Analogo: Stai cercando le chiavi in un cassetto. Tieni un dito (la flag trovato = false) alzato. Appena le trovi, abbassi il dito (trovato = true) e smetti di cercare.
let isLoading = false; // Flag: "Stiamo caricando dati?"
let hasError = false; // Flag: "C'è stato un errore?"
function fetchData() {
isLoading = true;
mostraSpinner(); // Mostra l'icona di caricamento
// ...simula chiamata di rete...
setTimeout(() => {
if (operazioneFallita) {
hasError = true;
}
isLoading = false;
nascondiSpinner();
aggiornaUI();
}, 2000);
}
Variabili di Stato
Questa è l'evoluzione di una flag booleana. Invece di un semplice true/false, una variabile di stato tiene traccia di in quale "modalità" si trova la tua applicazione.
Analogo: Un semaforo. Non è solo "acceso/spento", ma ha stati precisi: "rosso", "giallo", "verde".
// Stati di un form
let formState = "editing"; // Possibili stati: "editing", "submitting", "submitted", "error"
function gestisciForm() {
switch(formState) {
case "editing":
abilitaCampi();
nascondiSpinner();
break;
case "submitting":
disabilitaCampi();
mostraSpinner();
break;
case "submitted":
mostraMessaggioSuccesso();
break;
case "error":
mostraMessaggioErrore();
abilitaCampi();
break;
}
}
Usare una variabile di stato previene bug, come permettere all'utente di cliccare "Invia" (submitting) mentre sta già inviando.
Configuration Objects Pattern - Il Pannello di Controllo
Questo pattern consiste nel raggruppare tutte le tue impostazioni e "numeri magici" in un unico oggetto const all'inizio del file.
Analogo: Invece di avere post-it con password e impostazioni sparsi per tutto l'ufficio, li tieni tutti in un unico pannello di controllo chiuso a chiave.
// CATTIVO: Numeri magici sparsi
function checkTentativi(tentativi) {
if (tentativi > 3) { ... }
}
fetch("https://api.example.com/v1/users");
// BUONO: Oggetto di configurazione
const CONFIG = {
API_URL: "https://api.example.com/v1",
MAX_RETRIES: 3,
TIMEOUT_MS: 5000,
MESSAGES: {
error: "Si è verificato un errore",
loading: "Caricamento in corso..."
}
};
// Ora il codice è pulito e manutenibile
function checkTentativi(tentativi) {
if (tentativi > CONFIG.MAX_RETRIES) { ... }
}
fetch(`${CONFIG.API_URL}/users`);
Se un domani l'API cambia o vuoi cambiare il numero massimo di tentativi, modifichi un solo posto.
Gestione Errori con try-catch
Il codice fallirà. È una certezza. try-catch è la tua rete di sicurezza.
Analogo: Sei un trapezista. Il try è il tuo numero acrobatico. Il catch è la rete di sicurezza sotto di te. Puoi provare il salto (codice rischioso) senza paura di sfracellarti al suolo (far crashare l'intera applicazione).
function parseJSON(jsonString) {
try {
// 1. Prova a eseguire questo codice rischioso
const data = JSON.parse(jsonString);
console.log("Parsing riuscito:", data);
return data;
} catch (error) {
// 2. Se *qualsiasi cosa* nel 'try' fallisce,
// l'esecuzione salta immediatamente qui.
console.error("ERRORE! JSON non valido:", error.message);
// 'error' è un oggetto che contiene i dettagli dell'errore
return null; // Ritorna un valore sicuro
} finally {
// 3. (Opzionale) Eseguito *sempre*,
// sia che il try riesca o che il catch scatti.
// Utile per pulizia, es. nascondere uno spinner.
console.log("Tentativo di parsing completato.");
}
}
parseJSON('{"nome": "Mario"}'); // Riuscito
parseJSON('{nome: "Mario"}'); // Fallisce (mancano le virgolette sulla chiave), ma non crasha!
35. UX Avanzata: Performance e Adattabilità
Non trattare tutti gli utenti allo stesso modo. Un utente con l'ultimo iPhone connesso alla Fibra di casa ha superpoteri che un utente su un vecchio Android in galleria non ha. Il tuo codice deve adattarsi.
A. navigator.connection - Il "Tatto" del Browser 📶
Cosa fa: Permette al codice di "sentire" la qualità della connessione dell'utente e decidere quanto pesanti devono essere i dati da scaricare.
const connection = navigator.connection;
// Rileviamo se l'utente vuole risparmiare dati o ha una connessione lenta
const isSlow = connection ? (connection.saveData || connection.effectiveType.includes('2g')) : false;
const itemsToLoad = isSlow ? 5 : 20; // 5 elementi se lento, 20 se veloce
const imageQuality = isSlow ? 'low' : 'high'; // Immagini sgranate ma veloci vs HD
console.log(`Carico ${itemsToLoad} elementi in qualità ${imageQuality}`);
Analogia: Netflix 📺 Hai notato che se la rete rallenta, Netflix non si blocca ma abbassa la qualità del video (diventa un po' sfuocato)? Ecco, tu stai facendo la stessa cosa: invece di bloccare l'utente, gli dai un'esperienza "leggera" ma funzionante.
B. Intersection Observer - Lo "Scroll Infinito" ♾️
Cosa fa: Invece di costringere l'utente a cliccare "Carica altri" (frizione), carica automaticamente i nuovi contenuti quando l'utente arriva in fondo alla pagina.
Come funziona: Crei una "sentinella" (un elemento invisibile) in fondo alla lista. Quando questa sentinella entra nello schermo, scatta il caricamento.
// 1. La sentinella (in fondo all'HTML)
// <div id="sentinella"></div>
// 2. L'Osservatore
const observer = new IntersectionObserver((entries) => {
// Se la sentinella è visibile...
if (entries[0].isIntersecting) {
console.log("Siamo in fondo! Carica nuovi post...");
fetchMoreData(); // La tua funzione che fa il fetch
}
});
// 3. Inizia a osservare
const sentinella = document.getElementById('sentinella');
observer.observe(sentinella);
Analogia: The Truman Show (o un videogioco open world) 🌍 Il mondo non esiste tutto subito. Il mondo viene "costruito" solo un attimo prima che il protagonista ci posi lo sguardo. Se non guardi, non esiste. Questo risparmia risorse enormi!
Regola d'oro:
- Button "Carica altro": Sicuro ma noioso (Frizione alta).
- Infinite Scroll: Moderno e fluido (Frizione zero), ma devi gestire bene la memoria!
36. Best Practice di Stile e Qualità
Best Practices - Naming Convention
I nomi nel tuo codice sono importanti quanto il codice stesso. Devono raccontare una storia. Un nome ben scelto elimina la necessità di un commento.
Analogo: È la differenza tra etichettare una scatola "ROBA" e etichettarla "Fatture Elettriche 2023".
-
Costanti globali (Configurazione):
UPPER_SNAKE_CASE(Tutto maiuscolo, con underscore).const MAX_ATTEMPTS = 3;const API_KEY = "abc123"; -
Variabili e Funzioni:
camelCase(Inizia minuscolo, ogni nuova parola maiuscola).let userName = "Mario";function calculateTotal() {} -
Classi (Stampi):
PascalCase(Inizia maiuscolo).class UserAccount {}class ShoppingCart {} -
Nomi Semantici (che parlano):
- Booleani (Flag): Iniziali come domande:
isVisible,hasPermission,canEdit. - Funzioni: Verbi che descrivono l'azione:
fetchData(),validateEmail(),renderComponent(). - Array: Nomi plurali:
users,products,items. - Oggetti: Nomi singolari descrittivi:
user,product,configuration.
- Booleani (Flag): Iniziali come domande:
Testing Incrementale
Analogo: Assaggiare il sugo mentre cucini. Non scrivere 100 righe di codice e poi premere "play" sperando che tutto funzioni. È una ricetta per il disastro.
Il flusso di lavoro professionale è scrivi-testa-scrivi-testa:
- Scrivi 3 righe (es. una funzione vuota).
- Testa (
console.log("Funzione chiamata")). - Scrivi altre 5 righe (la logica interna).
- Testa (
console.log("Risultato intermedio:", risultato)). - Finisci la funzione.
- Testa (
console.log("Risultato finale:", finale)).
console.log è il tuo strumento di debug più potente. Usalo. Sempre.
Separazione delle Responsabilità (SoC)
Questo è un principio di design fondamentale. Ogni "pezzo" del tuo codice (funzione, classe, modulo) deve avere una sola, chiara responsabilità.
Analogo: In un ristorante, lo chef cucina, il cameriere prende gli ordini, il cassiere gestisce i soldi. È un disastro se lo chef deve anche prendere gli ordini e pulire i tavoli.
// CATTIVO: La funzione "tuttofare" 👎
function processUserData(userData) {
// 1. Valida...
if (!userData.email) return false;
// 2. Salva...
database.save(userData);
// 3. Invia email...
sendEmail(userData.email);
// 4. Aggiorna UI...
updateUI(userData);
}
// BUONO: Funzioni specializzate 👍
function validateUser(userData) { ... }
function saveUser(userData) { ... }
function notifyUser(email) { ... }
function updateUserUI(userData) { ... }
// Funzione "direttore d'orchestra"
function processUser(userData) {
if (!validateUser(userData)) return;
saveUser(userData);
notifyUser(userData.email);
updateUserUI(userData);
}
Questo codice è più facile da testare, debuggare e riutilizzare.
style.display vs classList (Best Practice di SoC)
Questo è un esempio perfetto di Separazione delle Responsabilità.
-
JavaScript (Logica) gestisce lo stato (es. "il menu è aperto?").
-
CSS (Presentazione) gestisce l'aspetto (es. "se il menu è aperto, mostralo").
-
style.display(Approccio non ottimale):elemento.style.display = "block";Analogo: Il JS scavalca il CSS e vernica a mano l'elemento. Mescola le responsabilità. È difficile aggiungere un'animazione (dovresti farla in JS) ed è difficile da sovrascrivere. -
classList(Approccio Migliore):elemento.classList.add("is-visible");Analogo: Il JS attacca un'etichetta (.is-visible) all'elemento. Il CSS, in un file separato, vede quell'etichetta e decide cosa fare./* CSS */
.menu { display: none; opacity: 0; }
.menu.is-visible { display: block; opacity: 1; transition: opacity 0.3s; }Ora puoi cambiare l'animazione o l'aspetto modificando solo il CSS, senza mai toccare il JavaScript.
innerHTML = vs innerHTML += (Performance)
-
innerHTML = "..."(Assegnazione): OK. Cosa fa: Cancella tutto il contenuto vecchio e lo sostituisce con quello nuovo. È un'operazione singola ed efficiente. -
innerHTML += "..."(Concatenazione): PESSIMA PERFORMANCE ❌ Cosa fa: Per aggiungere un elemento:- Il browser legge tutto l'HTML esistente e lo trasforma in una stringa.
- Aggiunge il tuo nuovo pezzo di stringa.
- Distrugge tutti i nodi DOM esistenti.
- Riesegue il parsing e ricrea tutti i nodi da zero (vecchi + nuovo).
*Analogo (
+=): Per aggiungere un solo libro a una libreria, svuoti completamente tutti gli scaffali, butti via i vecchi libri, e poi rimetti dentro le copie dei vecchi libri più quello nuovo. È follia.
-
Soluzione (per aggiungere): Usa
createElement()eappendChild(). Analogo: Prendi il nuovo libro e lo metti nello scaffale. Finito. Non tocchi gli altri.
.className vs .classList (Best Practice)
-
.className(L'Arma Spuntata): È una stringa. Se un elemento haclass="vecchia-classe"e tu faiel.className = "nuova-classe", hai cancellato la vecchia classe. -
.classList(Il Kit Chirurgico): È un oggetto speciale con metodi precisi. È il modo moderno e sicuro.el.classList.add("nuova");
el.classList.remove("vecchia");
el.classList.toggle("attivo"); // Aggiunge se non c'è, rimuove se c'èRegola: Usa sempre
classList.
.textContent vs innerHTML (Sicurezza)
Analogo: textContent è un pennarello (sicuro). innerHTML è una penna magica di Harry Potter (potente ma pericolosa).
-
.textContent(La Scelta Sicura ✅)- Inserisce solo testo puro.
- Se un utente scrive
<script>alert('attaccato!')</script>nel suo nome,textContentlo tratterà come testo innocuo e mostrerà letteralmente la stringa<script>...sulla pagina. - Usa questo di default per qualsiasi dato proveniente da un utente.
-
.innerHTML(La Scelta Pericolosa ⚠️)- Interpreta ed esegue qualsiasi tag HTML nella stringa.
- Se un utente scrive
<script>...e tu lo inserisci coninnerHTML, lo script verrà eseguito. Questo è il buco di sicurezza n. 1 del web (Cross-Site Scripting - XSS). - Regola: Usa
innerHTMLsolo se 1) sei tu a scrivere l'HTML o 2) la fonte è 100% sicura e fidata.
37. Immutabilità e Stile
Immutabilità (Concetto Generale)
Questo è un pattern fondamentale per scrivere codice prevedibile. Analogo: La differenza tra modificare un documento master originale (Mutazione) e fare una fotocopia e modificare quella (Immutabilità).
-
Mutazione ❌ (Il Male): Modifichi un array o un oggetto originale.
function aggiungiUtente(utenti) {
utenti.push({ nome: "Nuovo" }); // Muta l'array originale!
return utenti;
}
const miaLista = [{ nome: "Mario" }];
const nuovaLista = aggiungiUtente(miaLista);
// Problema: ora 'miaLista' è cambiata! [ {nome: "Mario"}, {nome: "Nuovo"} ]
// Qualsiasi altra parte del codice che usava 'miaLista' ora è "rotta"
// o ha dati inaspettati. Questo si chiama "Effetto Collaterale" (Side Effect). -
Immutabilità ✅ (Il Bene): Crei una copia con le modifiche e restituisci la copia. L'originale rimane intatto.
function aggiungiUtente(utenti) {
// Usa lo Spread Operator per fare una fotocopia
const nuovaLista = [...utenti, { nome: "Nuovo" }];
return nuovaLista;
}
const miaLista = [{ nome: "Mario" }];
const nuovaLista = aggiungiUtente(miaLista);
// 'miaLista' è ancora [ {nome: "Mario"} ] (intatta!)
// 'nuovaLista' è [ {nome: "Mario"}, {nome: "Nuovo"} ]Questo codice è prevedibile, sicuro e più facile da debuggare.
.sort() (Distruttivo) vs .toSorted() (Immutabile)
Questo è l'esempio perfetto di Immutabilità.
.sort()❌: È un metodo distruttivo. Modifica (muta) l'array originale..toSorted()✅: È un metodo moderno e immutabile. Restituisce una nuova copia ordinata, lasciando l'originale intatto.- (Lo stesso vale per
reverse()vstoReversed()esplice()vstoSpliced()).
Stile: Leggibilità vs Concisa (One-liner vs Multi-line)
Analogo: Un "one-liner" (codice su una riga) è come cercare di essere "furbi" e parlare in modo super-compatto. Il codice multi-line "racconta una storia" chiara.
-
One-liner (Conciso ma difficile da debuggare):
const media = array.map(n => n * 2).filter(n => n > 10).reduce((a, b) => a + b, 0);Dove metticonsole.logper vedere i risultati intermedi? Non puoi. -
Multi-line (Leggibile e facile da debuggare):
const mappato = array.map(n => n * 2);
// Facile da debuggare!
console.log("Dopo map:", mappato);
const filtrato = mappato.filter(n => n > 10);
console.log("Dopo filter:", filtrato);
const media = filtrato.reduce((a, b) => a + b, 0);
Verdetto: La leggibilità e la facilità di debug battono quasi sempre la furbizia della concisione. Scrivi codice che anche un "te stesso" assonnato tra 6 mesi possa capire.
Algoritmo di Scambio (Swap)
Come scambiare i valori di due variabili.
-
Classic (
temp) (La "Giostra" a Tre Posti): Analogo: Devi scambiare due persone (A e B) su due sedie, ma non possono alzarsi insieme. Hai bisogno di una sedia temporanea (temp).- A si sposta su temp.
- B si sposta sulla sedia di A.
- A (che era su temp) si sposta sulla sedia di B.
const temp = a;
a = b;
b = temp; -
Destructuring (Moderno, ES6): Analogo: Magia. Le due persone si scambiano di posto istantaneamente.
[a, b] = [b, a];Questo è più pulito, conciso e fa la stessa identica cosa.
38. Evoluzione del Codice
Il tuo codice non nasce mai perfetto. Evolve. Capire questi passaggi ti aiuta a scrivere codice migliore fin dall'inizio.
-
Da Hardcoded a Dinamico:
- Prima:
console.log("Benvenuto Mario!"); - Dopo:
const nome = prompt("Come ti chiami?"); console.log(`Benvenuto ${nome}\!`);Il codice smette di avere valori "scolpiti" e inizia a usare variabili.
- Prima:
-
Da Ripetitivo a DRY (Don't Repeat Yourself):
- Prima: Copi e incolli lo stesso blocco di 10 righe in tre punti diversi.
- Dopo: Crei una sola funzione con quelle 10 righe e la chiami in tre punti diversi.
- Vantaggio: Se devi fare una modifica, la fai in un solo posto.
-
Da Procedurale a Event-Driven:
- Prima (Procedurale): Il codice esegue tutto in ordine, dall'alto in basso, una sola volta, e poi finisce.
- Dopo (Event-Driven): Il codice carica le sue funzioni e poi... aspetta. Non fa nulla finché l'utente non fa qualcosa (es.
addEventListener("click", ...)). Questo è il modello di quasi tutto il web.
-
Da Globale a Modulare:
- Prima (Globale): Tutte le tue variabili (
punteggio,vite,nomeUtente) sono nella "piazza pubblica" (Global Scope), dove chiunque può toccarle e romperle. - Dopo (Modulare): Raggruppi le variabili correlate in "case" (Oggetti) o "fabbriche" (Classi) che le proteggono.
// Da così:
let punteggio = 0;
let vite = 3;
function aumentaPunteggio() { ... }
// A così:
const Game = {
punteggio: 0,
vite: 3,
aumentaPunteggio() { ... },
perdiVita() { ... }
};Questo protegge i tuoi dati e rende il codice infinitamente più organizzato.
- Prima (Globale): Tutte le tue variabili (
Pattern Avanzati e Algoritmi
39. Regex - Il Linguaggio dei Pattern
Le Espressioni Regolari (Regex o RegExp) sono come un metal detector super sofisticato per il testo. Mentre un metal detector normale trova solo "metallo", una regex può essere programmata per trovare qualsiasi pattern tu possa descrivere: indirizzi email, numeri di telefono, date, codici fiscali, parole duplicate, o anche pattern complessi come "tutte le parole che iniziano con 'A' e finiscono con 'o'".
Immagina di dover trovare tutti i numeri di telefono in un documento di 1000 pagine: manualmente ci metteresti giorni, una regex lo fa in millisecondi.
Anatomia di una Regex
Una regex è racchiusa tra due slash /pattern/, come una formula matematica tra parentesi. Ma questi slash sono più che semplici delimitatori: sono il confine tra il mondo normale di JavaScript e il mondo magico dei pattern.
// 1. Regex Letterale (più comune e performante)
// Pensa a questo come a /ciao/
const regex = /ciao/i; // Cerca "ciao", ignorando maiuscole/minuscole
// 2. Costruttore RegExp (usato quando il pattern è dinamico)
const parolaDaCercare = "mondo";
const regexDinamica = new RegExp(parolaDaCercare, "i"); // Cerca "mondo"
// Come si usa?
const testo = "Ciao Mondo, come stai?";
// .test() - Il Metal Detector (Sì/No)
// Risponde solo: "C'è o non c'è?" Restituisce true o false.
console.log(regex.test(testo)); // true
console.log(regexDinamica.test(testo)); // true
// .match() - L'Estrattore (Cosa hai trovato?)
// String.prototype.match() ti dà i risultati.
console.log(testo.match(regex)); // ["Ciao"]
console.log(testo.match(regexDinamica)); // ["Mondo"]
Flags - I Modificatori Globali
Le flag sono come interruttori che cambi sul tuo metal detector. Vanno dopo lo slash finale e cambiano il comportamento globale della ricerca.
-
g(global): È l'interruttore "Trova Tutti". Senzag, la regex si ferma al primo match che trova. Cong, continua a cercare fino alla fine della stringa, restituendo tutti i match."ciao ciao".match(/ciao/); // ["ciao"] (si ferma al primo)
"ciao ciao".match(/ciao/g); // ["ciao", "ciao"] (trova tutto) -
i(case-insensitive): L'interruttore "Ignora Maiuscole/Minuscole". Tratta "A" e "a" come se fossero lo stesso carattere./javascript/.test("JavaScript"); // false
/javascript/i.test("JavaScript"); // true -
m(multiline): L'interruttore "Multi-Riga". Di default, i caratteri speciali^(inizio) e$(fine) funzionano solo sull'intera stringa. Conm,^e$matchano l'inizio e la fine di ogni singola riga (separata da\n). -
s(dotAll): L'interruttore "il Punto è Tutto". Di default, il.(punto) matcha qualsiasi carattere tranne l' "a capo" (\n). Cons, il.matcha davvero tutto, incluso l' "a capo".
Combinare le Flag: Puoi attaccarle tutte insieme, in qualsiasi ordine.
/pattern/gi (Globale + Case-Insensitive)
Caratteri Speciali - I Superpoteri delle Regex
Alcuni caratteri nelle regex hanno significati speciali, come simboli magici in un incantesimo. Questi sono i tuoi strumenti principali:
-
.(Il Jolly): Il punto (dot) matcha qualsiasi carattere singolo (lettera, numero, spazio, simbolo), tranne l' "a capo" (a meno che non usi la flags)./c.ao/.test("ciao"); // true
/c.ao/.test("c9ao"); // true
/c.ao/.test("c ao"); // true
/c.ao/.test("cao"); // false (manca un carattere) -
^(L'Ancora di Inizio): Matcha l'inizio della stringa. Analogo: Dice "la stringa deve iniziare con questo"./^Ciao/.test("Ciao mondo"); // true
/^Ciao/.test("Ehi, Ciao"); // false (non inizia con Ciao) -
$(L'Ancora di Fine): Matcha la fine della stringa. Analogo: Dice "la stringa deve finire con questo"./mondo$/.test("Ciao mondo"); // true
/mondo$/.test("mondo ciao"); // false (non finisce con mondo)
// Combinati: /^Ciao$/ testa ESATTAMENTE "Ciao" -
|(L'Alternativa / OR): Il simbolo "pipe" significa "o". Analogo: È un bivio. "Prendi questa strada O quest'altra"./cane|gatto/.test("Mi piace il cane"); // true
/cane|gatto/.test("Mi piace il gatto"); // true
/cane|gatto/.test("Mi piace il topo"); // false
Character Classes [] - Il Club Esclusivo
Le parentesi quadre creano un "club" di caratteri. Il pattern matcha se trova UNO QUALSIASI dei membri del club in quella posizione.
-
Set di Caratteri (Il Club):
/[aeiou]/matcha una singola vocale./c[aeiou]t/.test("cat"); // true ('a' è nel club)
/c[aeiou]t/.test("cot"); // true ('o' è nel club)
/c[aeiou]t/.test("c9t"); // false ('9' non è nel club)Utile per "leetspeak":
m[o0]n[e3]ymatcha "money", "m0ney", "m0n3y", ecc. -
Range (Il Trattino): Per non scrivere
[0123456789], usi un trattino./[a-z]/// Qualsiasi lettera minuscola/[A-Z]/// Qualsiasi lettera maiuscola/[0-9]/// Qualsiasi cifra/[a-zA-Z0-9_]/// Alfanumerico più underscore (identico a\w) -
Negazione (Il Buttafuori
^): Se il primo carattere dentro[]è^, significa "matcha qualsiasi carattere TRANNE quelli in questo club"./[^aeiou]/// Matcha qualsiasi consonante (o numero, o spazio...)/[^0-9]/// Matcha qualsiasi cosa non sia una cifra
Classi Predefinite - Le Scorciatoie
Per i "club" più comuni, JavaScript ti dà delle scorciatoie (o "macro"):
-
\d(Digit): Qualsiasi cifra. Equivalente a:[0-9] -
\D(Non-Digit): Qualsiasi cosa non sia una cifra. Equivalente a:[^0-9] -
\w(Word Character): Qualsiasi carattere alfanumerico (A-Z, a-z, 0-9) più l'underscore (_). Equivalente a:[A-Za-z0-9_]Attenzione: Non include il trattino-! -
\W(Non-Word Character): Qualsiasi cosa non sia un\w(spazi, punteggiatura, simboli). -
\s(Space): Qualsiasi carattere di spazio bianco (spazio, tab\t, "a capo"\n). -
\S(Non-Space): Qualsiasi cosa non sia uno spazio bianco. -
\b(Word Boundary): Questo è speciale. È un'"ancora" a larghezza zero. Matcha la posizione tra un\we un\W(cioè, il confine di una parola). Analogo: È come cercare la "fine del marciapiede" di una parola./\bcat\b/.test("the cat sat"); // true ('cat' è una parola intera)
/\bcat\b/.test("category"); // false ('cat' è *dentro* una parola)
Quantificatori - Quante Volte?
I quantificatori specificano quante volte l'elemento immediatamente precedente deve ripetersi.
-
?(Zero o Uno): Rende l'elemento opzionale. Analogo: "Colore o Colore? Non importa"./colou?r/.test("color"); // true (0 'u')
/colou?r/.test("colour"); // true (1 'u') -
+(Uno o Più): L'elemento deve esserci almeno una volta. Analogo: "Voglio un numero!"/\d+/.test("12345"); // true (ci sono 5 cifre)
/\d+/.test("abc"); // false (non c'è *almeno una* cifra) -
*(Zero o Più): L'elemento può esserci o non esserci, anche tante volte. Analogo: "Spazi? Forse sì, forse no, forse tanti"./ab*c/.test("ac"); // true (0 'b')
/ab*c/.test("abbbc"); // true (3 'b')
/\s*/.test(""); // true (0 spazi) -
{n}(Esattamentenvolte):/\d{4}/// Matcha esattamente 4 cifre (es. un PIN) -
{n,m}(Danamvolte):/\d{2,4}/// Matcha da 2 a 4 cifre -
{n,}(Almenonvolte):/\d{3,}/// Matcha almeno 3 cifre
Escape \ - Quando il Speciale Diventa Normale
Il backslash \ è il tuo "disattivatore" di poteri speciali. Se vuoi cercare letteralmente un carattere che ha un significato speciale (come ., +, *, ?, $), devi "escaparlo" mettendogli un \ davanti.
// SBAGLIATO: Voglio cercare "10.00"
/10.00/.test("prezzo 10.00"); // true
/10.00/.test("prezzo 10X00"); // true! (Perché '.' è un Jolly)
// CORRETTO: Escapo il punto
/10\.00/.test("prezzo 10.00"); // true
/10\.00/.test("prezzo 10X00"); // false
// Altri esempi:
// Per cercare un "+" letterale: \+
// Per cercare un "$" letterale: \$
// Per cercare un "\" letterale: \\ (doppio escape)
Anchors (^, $) vs Spazio (\s) (Soluzione parola intera)
-
Il Problema: Vuoi trovare
wordma solo se è una parola intera./\sword\s/(con spazi) è una trappola!- Matcha:
" in word here "(OK) - Non matcha:
"word"(all'inizio/fine, non ha spazi intorno)
- Matcha:
-
Le Ancore (
^,$): Come visto,^e$sono "asserzioni di posizione" (zero-width). Verificano una posizione (inizio/fine stringa), non consumano un carattere. -
La Soluzione (Combinata): Per matchare una parola circondata da spazi OPPURE ai confini del testo, devi usare un gruppo OR. E per non "catturare" (vedi sotto) quegli spazi, usiamo un gruppo non-capturing
(?:...)./(\s|^)word(\s|$)/(Versione semplice con cattura)/(?:\s|^)word(?:\s|$)/(Versione ottimizzata)(?:\s|^)= "matcha uno spazio OPPURE l'inizio della stringa"(?:\s|$)= "matcha uno spazio OPPURE la fine della stringa" Questo pattern matchawordin tutti questi casi:"word"(OK)"word here"(OK)"see word"(OK)"see word here"(OK)
Gruppi: Capturing () vs Non-Capturing (?:...)
Le parentesi () sono fondamentali, ma fanno due cose contemporaneamente:
- Raggruppano: Permettono di applicare un quantificatore (come
?,+) a un gruppo di caratteri.abc?(solo lacè opzionale)(abc)?(l'intero gruppo "abc" è opzionale)
- Catturano: Memorizzano il pezzo di stringa che ha matchato quel gruppo.
Analogo: Il Pullman 🚌
-
()(Gruppo di Cattura): È un pullman. Raggruppa gli studenti (li tiene insieme) E il professore fa una foto 📸 di quel gruppo specifico per l'annuario (lo cattura).const testo = "user@example.com";
const match = testo.match(/(\w+)@(\w+\.\w+)/);
// match[0] = "user@example.com" (match intero)
// match[1] = "user" (Foto 📸 del Gruppo 1)
// match[2] = "example.com" (Foto 📸 del Gruppo 2) -
(?:...)(Gruppo Non-Capturing): A volte vuoi solo raggruppare, ma non ti interessa la foto (non vuoi salvare quel pezzo). La sintassi(?:...)fa questo. Analogo: È un pullman (raggruppa) ma il professore non fa la foto (non cattura).// Voglio solo sapere se inizia con "http" o "https",
// ma non mi interessa *quale* dei due.
const regex = /(?:http|https):\/\//;
const match = "https://google.com".match(regex);
// match[0] = "https://"
// match[1] = undefined (Nessuna foto 📸!) -
Perché usarlo?
- Performance: Non spreca memoria per salvare "foto" che non userai.
- Chiarezza: Mantiene l'array dei risultati pulito, contenente solo i gruppi che volevi estrarre.
Regola: Usa
()solo se devi estrarre quel pezzo. Se devi solo raggruppare (per un|o un?), usa(?:...).
Lookahead e Lookbehind - Gli Occhi del Futuro
Questi sono pattern avanzati che "guardano" avanti o indietro senza consumare caratteri. Matchano una condizione in una posizione.
-
(?=...)(Lookahead Positivo): "Trova X, solo se è seguito da Y". Analogo: "Trova il numero, solo se vedi il simbolo € dopo."// Estrae solo il numero da un prezzo
/\d+(?=€)/.exec("costa 50€")[0]; // "50"
// "€" è la condizione, ma non fa parte del match. -
(?!...)(Lookahead Negativo): "Trova X, solo se NON è seguito da Y". -
(?<=...)(Lookbehind Positivo): "Trova X, solo se è preceduto da Y". Analogo: "Trova il numero, solo se vedi il simbolo $ prima."/(?<=€)\d+/.exec("costa €50")[0]; // "50" -
(?<!...)(Lookbehind Negativo): "Trova X, solo se NON è preceduto da Y".
Pattern del Mondo Reale - Le Regex Utili
Ecco alcuni pattern che ti troverai a usare spesso:
// EMAIL (semplificata ma efficace)
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// URL (semplificata)
const urlRegex = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(\S*)$/i;
// DATA ITALIANA (GG/MM/AAAA)
const dataRegex = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;
// ESTRAI NUMERI DA TESTO
const testo = "Ho 3 mele e 2 pere per €5.50";
const numeri = testo.match(/\d+(\.\d+)?/g); // ["3", "2", "5.50"]
// RIMUOVI TAG HTML (semplificata)
const testoSenzaHTML = htmlString.replace(/<[^>]*>/g, '');
// CAPITALIZZA PRIME LETTERE (usa una funzione nella replace!)
const capitalizza = str => str.replace(/\b\w/g, (lettera) => lettera.toUpperCase());
capitalizza("ciao mondo"); // "Ciao Mondo"
40. Call Stack e Ricorsione - La Torre di Piatti
Il Call Stack (o "Pila delle Chiamate") è il taccuino di JavaScript. È il suo "cervello" a breve termine, dove tiene traccia di quale funzione sta eseguendo in questo preciso istante e quali sono in "pausa", in attesa di essere completate.
Il Modello Mentale: La Pila di Piatti (LIFO)
L'analogia perfetta è una pila di piatti sporchi accanto al lavandino.
- Hai un piatto sporco (chiami la
funzioneA), lo metti sulla pila.[funzioneA] - Arriva un altro piatto (la
funzioneAchiama lafunzioneB), lo metti sopra il primo.[funzioneA, funzioneB] - Arriva un terzo piatto (la
funzioneBchiama lafunzioneC), lo metti in cima.[funzioneA, funzioneB, funzioneC] - Ora devi lavare. Quale lavi? L'ultimo che hai messo in cima (la
funzioneC). - Finito di lavare
C, la togli dalla pila (pop).[funzioneA, funzioneB] - Ora lavi
B, la togli dalla pila.[funzioneA] - Infine, lavi
A(il primo che avevi messo) e la pila è vuota.[]
Questo si chiama LIFO (Last In, First Out): l'ultimo piatto aggiunto è il primo ad essere lavato. Il Call Stack funziona esattamente così.
Vediamolo nel codice:
function prima() {
console.log("1. Inizio prima");
seconda();
console.log("5. Fine prima");
}
function seconda() {
console.log("2. Inizio seconda");
terza();
console.log("4. Fine seconda");
}
function terza() {
console.log("3. Eseguo terza");
}
// 1. Inizia l'esecuzione
prima();
// Output:
// 1. Inizio prima
// 2. Inizio seconda
// 3. Eseguo terza
// 4. Fine seconda
// 5. Fine prima
// Il Call Stack si è evoluto così:
// [] - (Vuoto)
// [prima] - (Entra 'prima')
// [prima, seconda] - ('prima' chiama 'seconda')
// [prima, seconda, terza] - ('seconda' chiama 'terza')
// [prima, seconda] - ('terza' finisce, viene tolta)
// [prima] - ('seconda' finisce, viene tolta)
// [] - ('prima' finisce, lo stack è vuoto)
L'Errore Famoso: Stack Overflow
Cosa succede se continui a mettere piatti sulla pila, all'infinito, senza mai lavare? La torre crolla. Questo è uno Stack Overflow: hai chiamato troppe funzioni (spesso una funzione che chiama se stessa all'infinito) senza mai farle finire, riempiendo il "taccuino" di JavaScript fino a farlo esplodere.
Ricorsione - La Funzione che Chiama Se Stessa
La ricorsione è quando una funzione risolve un problema chiamando se stessa con una versione "più piccola" del problema.
Analogo: Pensa alle matrioske russe 🪆. Per aprire la matrioska (risolvere il problema), devi aprirla e trovare... una matrioska più piccola (una versione più piccola del problema). Continui ad aprirle finché non trovi l'ultima, piccolissima bambola solida (il "caso base").
Una funzione ricorsiva ha due parti obbligatorie:
- Caso Base (La Matrioska Solida): La condizione di stop. È la versione più semplice del problema che può essere risolta senza un'altra chiamata. Senza questo, avrai uno Stack Overflow (la pila di piatti infinita).
- Caso Ricorsivo (Le Matrioske Intermedie): Il punto in cui la funzione "rompe" il problema in un pezzo più piccolo e chiama se stessa per risolverlo.
Esempio: Fattoriale (!)
Il fattoriale di n (scritto n!) è n * (n-1) * (n-2) * ... * 1.
Es. 4! = 4 * 3 * 2 * 1 = 24.
-
Versione Iterativa (con un
forloop):function fattorialeIterativo(n) {
let risultato = 1;
for (let i = n; i > 1; i--) {
risultato = risultato * i;
}
return risultato;
} -
Versione Ricorsiva (Elegante): La logica è:
4! = 4 * 3!... e3! = 3 * 2!... e2! = 2 * 1!.function fattoriale(n) {
// 1. CASO BASE (La matrioska solida)
if (n <= 1) {
return 1;
}
// 2. CASO RICORSIVO (n * versione più piccola)
return n * fattoriale(n - 1);
}
// Tracciamo fattoriale(4)
//
// STACK (Pila di piatti):
// [fattoriale(4)] -> deve aspettare fattoriale(3)
// [f(4), f(3)] -> deve aspettare fattoriale(2)
// [f(4), f(3), f(2)] -> deve aspettare fattoriale(1)
// [f(4), f(3), f(2), f(1)] -> f(1) è il CASO BASE!
//
// Ora la pila si "risolve" (lavi i piatti dall'alto):
// f(1) restituisce 1.
// [f(4), f(3), f(2)] -> f(2) riceve 1 e fa return 2 * 1 = 2
// [f(4), f(3)] -> f(3) riceve 2 e fa return 3 * 2 = 6
// [f(4)] -> f(4) riceve 6 e fa return 4 * 6 = 24
// [] -> Risultato finale: 24
Esempio: Conversione Decimale a Binario
Come converti un numero decimale (es. 10) in binario (es. "1010")? L'algoritmo è:
- Dividi il numero per 2.
- Annota il resto (sarà 0 o 1).
- Ripeti il processo con il quoziente.
- Continua finché il quoziente non è 0 o 1.
- Leggi i resti al contrario.
La ricorsione è perfetta per questo, perché il Call Stack "ricorda" i resti nell'ordine giusto per noi!
function decimalToBinary(num) {
// 1. CASO BASE (La matrioska solida)
if (num <= 1) {
return String(num); // Restituisce "1" o "0"
}
// 2. CASO RICORSIVO
const quoziente = Math.floor(num / 2);
const resto = num % 2;
// La magia: chiama la funzione sul quoziente (più piccolo)
// e attacca il resto *alla fine*.
return decimalToBinary(quoziente) + String(resto);
}
// Tracciamo decimalToBinary(10):
//
// STACK:
// [d(10)] -> deve aspettare d(5). Resto: 0
// [d(10), d(5)] -> deve aspettare d(2). Resto: 1
// [d(10), d(5), d(2)] -> deve aspettare d(1). Resto: 0
// [d(10), d(5), d(2), d(1)] -> d(1) è il CASO BASE!
//
// La pila si "risolve":
// d(1) restituisce "1".
// [d(10), d(5), d(2)] -> d(2) riceve "1" e fa return "1" + "0" = "10"
// [d(10), d(5)] -> d(5) riceve "10" e fa return "10" + "1" = "101"
// [d(10)] -> d(10) riceve "101" e fa return "101" + "0" = "1010"
// [] -> Risultato finale: "1010"
Ricorsione con Memorizzazione (Memoization)
Il Problema: La ricorsione pura può essere incredibilmente inefficiente.
Prendiamo l'esempio di Fibonacci (dove fib(n) = fib(n-1) + fib(n-2)).
Per calcolare fib(5), devi calcolare:
fib(4)efib(3)- Per
fib(4), devi calcolarefib(3)efib(2)... hai già calcolatofib(3)due volte! Perfib(40), calcoleraifib(2)milioni di volte.
La Soluzione (Memoization): Analogo: È come scrivere la risposta a un problema difficile su un post-it. La prossima volta che ti fanno la stessa identica domanda, non la ricalcoli da zero. Leggi semplicemente il post-it.
Usiamo un "cache" (un oggetto) per memorizzare i risultati già calcolati.
// Versione LENTA (Esponenziale)
function fibLento(n) {
if (n <= 1) return n;
return fibLento(n - 1) + fibLento(n - 2);
}
// Versione VELOCE (Memoization)
// Usiamo una IIFE (una funzione che si auto-chiama)
// per creare una "cache" privata che la funzione interna può usare.
const fibMemo = (function() {
const cache = {}; // Il nostro "bloc-notes" privato
return function fib(n) {
// 1. Controllo il bloc-notes (cache)
if (n in cache) {
return cache[n]; // Trovato! Leggo il post-it.
}
// 2. Caso base
if (n <= 1) {
return n;
}
// 3. Non trovato? Calcolo E memorizzo
const risultato = fib(n - 1) + fib(n - 2);
cache[n] = risultato; // Scrivo il risultato sul post-it
return risultato;
};
})(); // La () finale esegue la funzione esterna
console.time("Lento");
console.log(fibLento(40)); // Ci mette secondi!
console.timeEnd("Lento");
console.time("Veloce");
console.log(fibMemo(40)); // È istantaneo!
console.timeEnd("Veloce");
La ricorsione è un concetto elegante, e la memorizzazione la rende uno strumento pratico e potente.
41. Algoritmi Pratici - Le Ricette del Codice
Gli algoritmi sono il cuore della programmazione. Non sono codice, sono idee. Sono le "ricette" testate e collaudate che i programmatori usano da decenni per risolvere problemi comuni, come ordinare una lista o trovare un dato. Imparare questi pattern è come per uno chef imparare a fare la besciamella o un impasto base: sono i mattoni fondamentali per creare piatti (programmi) complessi.
Algoritmo di Conversione Decimale → Binario
Abbiamo già visto la versione ricorsiva di questo algoritmo (nella Sezione 19 sul Call Stack), che è elegante e sfrutta lo stack per "ricordare" i resti.
Esiste anche una versione iterativa (con un ciclo while), che è spesso più performante e non rischia uno "Stack Overflow" con numeri enormi. È l'implementazione "manuale" dello stesso concetto.
Analogo: Invece di usare le matrioske (ricorsione), usi un blocco note (binary) e un pallottoliere (input).
L'algoritmo è: "Dividi per 2, segna il resto, ripeti con il quoziente."
function decimalToBinary(input) {
if (input === 0) return "0"; // Caso base
let binary = ""; // Il nostro "blocco note" (stringa)
let numero = input; // Il nostro "pallottoliere"
// Continua finché il pallottoliere non è a zero
while (numero > 0) {
// 1. Qual è il resto della divisione per 2?
const resto = numero % 2; // Sarà 0 o 1
// 2. "Attacca" il resto *davanti* alla stringa
// (perché i resti si leggono al contrario)
binary = resto + binary;
// 3. Prepara il prossimo giro con il quoziente
numero = Math.floor(numero / 2);
}
return binary;
}
// Test
console.log(decimalToBinary(10)); // "1010"
console.log(decimalToBinary(255)); // "11111111"
Algoritmi di Ordinamento (Sorting)
Ordinare una lista è uno dei problemi più classici dell'informatica.
Bubble Sort - Il Più Semplice (ma Inefficiente)
Analogo: È come un barattolo di bolle. Le bolle più "leggere" (i numeri più piccoli) "salgono" lentamente verso l'inizio della lista.
- Come funziona: Scorre l'array, confrontando ogni elemento (
array[j]) con quello successivo (array[j+1]). Se sono nell'ordine sbagliato, li scambia (swap). Ripete questo intero processo più e più volte, finché l'array non è ordinato. - Performance: È terribilmente lento ($O(n^2)$). Se l'array raddoppia, il tempo di esecuzione quadruplica. Non usarlo mai in produzione, ma è fantastico per imparare gli "swap".
function bubbleSort(arr) {
const array = [...arr]; // Copia per non modificare l'originale
const n = array.length;
for (let i = 0; i < n - 1; i++) {
let swapped = false; // Ottimizzazione
for (let j = 0; j < n - i - 1; j++) {
// Confronta elementi adiacenti
if (array[j] > array[j + 1]) {
// Scambia (Swap) con destrutturazione
[array[j], array[j + 1]] = [array[j + 1], array[j]];
swapped = true;
}
}
// Ottimizzazione: se un intero giro non ha fatto scambi,
// l'array è già ordinato. Esci presto.
if (!swapped) break;
}
return array;
}
Quick Sort - Veloce ed Elegante (Divide et Impera)
Analogo: È come ordinare una biblioteca.
- Prendi un libro a caso (
pivot). - Dividi tutti gli altri libri in due mucchi:
sinistra(quelli che vengono prima del pivot nell'alfabeto) edestra(quelli che vengono dopo). - Affidi i due mucchi più piccoli a due assistenti, dicendo loro: "Fate la stessa identica cosa che ho fatto io" (Ricorsione!).
- Quando ti restituiscono i mucchi ordinati, li unisci:
[mucchiosinistra_ordinato, pivot, mucchiodestra_ordinato].
- Performance: È uno degli algoritmi più veloci in media ($O(n \log n)$). Si basa pesantemente sulla ricorsione (e quindi sul Call Stack!).
function quickSort(arr) {
// Caso Base: un array con 0 o 1 elemento è già ordinato
if (arr.length <= 1) return arr;
// 1. Scegli un Pivot (prendiamo l'ultimo)
const pivot = arr[arr.length - 1];
// 2. Dividi in due mucchi (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. Chiama ricorsivamente e unisci
return [...quickSort(left), pivot, ...quickSort(right)];
}
Algoritmi di Ricerca
Binary Search - Ricerca Binaria (Il Dizionario)
Questo è un algoritmo incredibilmente veloce per trovare un elemento, ma ha un prerequisito fondamentale: l'array DEVE essere già ordinato.
Analogo: È il modo in cui cerchi una parola in un dizionario.
- Apri il dizionario esattamente a metà.
- La parola che vedi (
mid) è quella che cerchi? Fantastico, hai finito. - La parola che cerchi viene dopo (è più grande)? Allora sai che è inutile guardare la prima metà del dizionario. Butti via mentalmente tutta la metà sinistra.
- La parola che cerchi viene prima (è più piccola)? Butti via tutta la metà destra.
- Ripeti il processo (apri a metà, confronta, butta via metà) sul mucchio rimasto.
- Performance: È velocissima ($O(\log n)$). Per trovare 1 elemento su un miliardo, ci mette al massimo 30 controlli (mentre un ciclo
forne farebbe in media 500 milioni).
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
// 1. Trova l'indice di mezzo
const mid = Math.floor((left + right) / 2);
// 2. Controlla se è lui
if (arr[mid] === target) {
return mid; // Trovato! Restituisci l'indice
}
// 3. È più grande? Butta via la metà sinistra
if (arr[mid] < target) {
left = mid + 1;
}
// 4. È più piccolo? Butta via la metà destra
else {
right = mid - 1;
}
}
return -1; // Non trovato
}
const sorted = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
console.log(binarySearch(sorted, 7)); // 3 (indice)
console.log(binarySearch(sorted, 6)); // -1 (non trovato)
Algoritmi su Stringhe
Palindromo - Verifica se una Stringa è Palindroma
Definizione: Una stringa che si legge allo stesso modo nei due sensi (es. "anna", "i topi non avevano topi"). Sfida: Bisogna ignorare maiuscole/minuscole, spazi e punteggiatura.
function isPalindrome(str) {
// 1. Pulisci la stringa
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
// 2. Metodo "Pigro": confronta con il suo inverso
const reversed = cleaned.split('').reverse().join('');
return cleaned === reversed;
}
// Metodo "Due Puntatori" (più performante)
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; // Non corrispondono
}
left++;
right--;
}
return true; // Sono arrivati al centro
}
console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
Anagrammi - Verifica se Due Stringhe sono Anagrammi
Definizione: Due stringhe che usano esattamente le stesse lettere, ma in ordine diverso (es. "listen", "silent").
// Metodo 1: Il Trucco dell'Ordinamento
function areAnagramsSort(str1, str2) {
// Funzione helper per pulire e ordinare
const cleanSort = s => s.toLowerCase()
.replace(/[^a-z]/g, '')
.split('')
.sort()
.join('');
return cleanSort(str1) === cleanSort(str2);
}
// Metodo 2: Mappa di Frequenza (Vedi Parte I, ultima sezione del capitolo 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 = {};
// Conta le lettere della prima stringa
for (const char of s1) {
count[char] = (count[char] || 0) + 1;
}
// Sottrai le lettere della seconda stringa
for (const char of s2) {
if (!count[char]) return false; // Lettera extra
count[char]--;
}
return true; // Se tutti i conteggi sono a 0, è un anagramma
}
console.log(areAnagramsSort("listen", "silent")); // true
Algoritmi Numerici
Numeri Primi - Verifica e Generazione
Definizione: Un numero maggiore di 1, divisibile solo per 1 e per se stesso.
// Verifica se UN numero è primo (versione ottimizzata)
function isPrime(n) {
if (n <= 1) return false;
if (n <= 3) return true;
// Ottimizzazione: escludi subito i multipli di 2 e 3
if (n % 2 === 0 || n % 3 === 0) return false;
// Ottimizzazione: controlla solo fino alla radice quadrata
for (let i = 5; i * i <= n; i += 6) {
// Controlla i=5 e i+2=7, poi i=11 e i+2=13, ecc.
if (n % i === 0 || n % (i + 2) === 0) {
return false;
}
}
return true;
}
// Crivello di Eratostene - Genera TUTTI i primi fino a 'max'
function sieveOfEratosthenes(max) {
// 1. Crea un array di "sì" (true)
const prime = new Array(max + 1).fill(true);
prime[0] = prime[1] = false; // 0 e 1 non sono primi
for (let i = 2; i * i <= max; i++) {
// 2. Se 'i' è ancora "sì" (è primo)...
if (prime[i]) {
// 3. ...allora "cancella" tutti i suoi multipli
for (let j = i * i; j <= max; j += i) {
prime[j] = false;
}
}
}
// 4. Raccogli i risultati
const primes = [];
prime.forEach((èPrimo, numero) => {
if (èPrimo) primes.push(numero);
});
return primes;
}
Fibonacci - La Sequenza Aurea
Definizione: Ogni numero è la somma dei due precedenti (0, 1, 1, 2, 3, 5, 8...). La versione ricorsiva con memoization è ottima (vista nella Sezione 19). La versione iterativa (con loop) è la più efficiente in assoluto.
function fibonacciIterative(n) {
if (n <= 1) return n;
let prev = 0;
let curr = 1;
for (let i = 2; i <= n; i++) {
// La magia dello swap con destrutturazione:
// Il nuovo 'prev' diventa il 'curr'
// Il nuovo 'curr' diventa (vecchio prev + vecchio curr)
[prev, curr] = [curr, prev + curr];
}
return curr;
}
console.log(fibonacciIterative(7)); // 13
MCD/MCM (Algoritmo di Euclide)
- MCD (GCD - Greatest Common Divisor): Il numero più grande che divide entrambi.
- MCM (LCM - Least Common Multiple): Il numero più piccolo che è multiplo di entrambi.
L'Algoritmo di Euclide per il MCD è uno degli algoritmi più antichi e veloci.
Analogo: gcd(a, b) è a se b è 0. Altrimenti, è gcd(b, a % b).
// Algoritmo di Euclide (Ricorsivo)
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b);
}
// Formula per il MCM
function lcm(a, b) {
// (a * b) può essere enorme, meglio dividere prima
return (a / gcd(a, b)) * b;
}
console.log(gcd(48, 18)); // 6
console.log(lcm(21, 6)); // 42
42. Sanitizzazione e Validazione Input
La Validazione dell'input è la tua prima linea di difesa contro bug, dati corrotti e vulnerabilità di sicurezza. È come il controllo di sicurezza all'aeroporto: non puoi (e non devi) fidarti di ciò che l'utente ti passa. Devi controllare rigorosamente prima di far "salire a bordo" i dati nel tuo sistema.
- Validazione: È il processo di controllo. Risponde alla domanda: "Questi dati sono nel formato che mi aspetto?". (Es. "È un numero? È un'email valida?"). L'azione è accettare o rifiutare.
- Sanitizzazione: È il processo di pulizia. Risponde alla domanda: "Come posso rendere sicuri questi dati?". (Es. "Rimuovo i tag
<script>"). L'azione è modificare e pulire.
Validazione Robusta con Pattern Guards
Il pattern "Guard Clause" (o "Return Early") è il modo più pulito per scrivere funzioni di validazione.
Analogo: È come un buttafuori a una festa. Invece di far entrare tutti e poi cercare quelli che non vanno bene (if annidati), il buttafuori controlla i documenti all'ingresso. Se non hai il biglietto, ti rimbalza (return) subito. Solo chi ha tutti i requisiti arriva alla festa (la logica principale).
CATTIVO: La "Piramide della Rovina" (Pyramid of Doom) 👎
Questo codice è difficile da leggere. Il "percorso felice" (quello che fa il vero lavoro) è sepolto in fondo, dentro tre livelli di if.
function processData(data) {
if (data) {
if (data.isValid) {
if (data.value > 0) {
// ...finalmente, il codice che ci interessa...
// ...sepolto qui dentro...
return data.value * 2;
} else {
return null; // Caso di errore 3
}
} else {
return null; // Caso di errore 2
}
} else {
return null; // Caso di errore 1
}
}
BUONO: Pattern "Guard Clauses" 👍 Il codice è "piatto", leggibile e la logica principale è l'ultima cosa, non la più interna.
function processData(data) {
// Guardia 1: I dati esistono?
if (!data) {
return null; // Esci subito
}
// Guardia 2: I dati sono validi?
if (!data.isValid) {
return null; // Esci subito
}
// Guardia 3: Il valore è positivo?
if (data.value <= 0) {
return null; // Esci subito
}
// Se siamo arrivati qui, tutte le guardie ci hanno fatto passare.
// Il "percorso felice" è piatto e facile da leggere.
return data.value * 2;
}
Validazione di Input Numerici
Quando ricevi un numero da un <input>, ricorda che è sempre una stringa! Devi validarlo rigorosamente. Ecco una funzione robusta che usa le Guard Clauses:
function validateNumber(input, options = {}) {
// Imposta opzioni di default
const {
min = -Infinity,
max = Infinity,
integer = false, // Deve essere un intero?
positive = false // Deve essere > 0?
} = options;
// Guardia 1: È richiesto?
if (input === "" || input === null || input === undefined) {
return { valid: false, error: "Input richiesto" };
}
// Converti
const num = Number(input);
// Guardia 2: È un numero? (Usa Number.isNaN per sicurezza)
if (Number.isNaN(num)) {
return { valid: false, error: "Deve essere un numero" };
}
// Guardia 3: È un intero?
if (integer && !Number.isInteger(num)) {
return { valid: false, error: "Deve essere un numero intero" };
}
// Guardia 4: È positivo?
if (positive && num <= 0) {
return { valid: false, error: "Deve essere un numero positivo" };
}
// Guardia 5: Rispetta il minimo?
if (num < min) {
return { valid: false, error: `Il numero deve essere almeno ${min}` };
}
// Guardia 6: Rispetta il massimo?
if (num > max) {
return { valid: false, error: `Il numero non deve superare ${max}` };
}
// Se è arrivato qui, è valido!
return { valid: true, value: num };
}
// --- Esempio di utilizzo ---
const userInput = "25";
const result = validateNumber(userInput, {
min: 1,
max: 100,
integer: true
});
if (!result.valid) {
alert(result.error);
} else {
console.log("Numero valido:", result.value); // result.value è 25 (un numero!)
}
Rimozione Caratteri Speciali e Sanitizzazione
La sanitizzazione è il processo di pulizia dei dati. Non li rifiuti, li modifichi per renderli sicuri. Analogo: È come filtrare l'acqua prima di berla. Togli lo sporco (i caratteri pericolosi) e tieni l'acqua (il contenuto sicuro).
Lo strumento principale è String.prototype.replace() con una Regex.
// Esempio 1: Sanitizzazione base (rimuove tutto tranne lettere, numeri, spazi)
function sanitizeBasic(str) {
// Regex: [^a-zA-Z0-9\s] -> "trova tutto ciò che NON (^)
// è una lettera (a-z, A-Z), un numero (0-9) o uno spazio (\s)"
// ...e rimpiazzalo con una stringa vuota (cancellalo).
return str.replace(/[^a-zA-Z0-9\s]/g, '');
}
console.log(sanitizeBasic("Ciao! Questo è un test al 100%?"));
// Output: "Ciao Questo è un test al 100"
// Esempio 2: Sanitizzatore per contesti diversi
const Sanitizer = {
// Pulisce per creare un ID o "slug" (es. per un URL)
forId(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Solo lettere, numeri, spazi, trattini
.replace(/\s+/g, '-') // Spazi -> trattini
.replace(/-+/g, '-') // Trattini multipli -> uno
.replace(/^-|-$/g, ''); // Rimuove trattini all'inizio/fine
},
// Il più importante: Sanitizzazione per HTML (Prevenire XSS)
// Non usare regex! È troppo facile sbagliare.
// Usa il trucco del 'textContent'!
forHTML(str) {
const div = document.createElement('div');
// Impostando textContent, il browser "uccide"
// qualsiasi tag HTML (es. <script>) e lo tratta come testo.
div.textContent = str;
// Rileggendo innerHTML, ottieni la versione "escapata" e sicura.
// <script> diventa <script>
return div.innerHTML;
}
};
console.log(Sanitizer.forId(" L'ultimo caffè -- a €2.50! "));
// Output: "lultimo-caff-a-250"
Validazione Email e Pattern Comuni
Per la validazione complessa, non reinventare la ruota. Usa pattern Regex collaudati.
const Validator = {
patterns: {
// Questa regex è lo standard "pratico" (RFC 5322).
// Non provare a scriverla da solo!
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
// Esempio di password (min 8, 1 maiuscola, 1 minuscola, 1 numero)
passwordStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/,
// Data italiana
dateIT: /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/
},
// Funzione di validazione semplice (Sì/No)
validate(type, value) {
if (!this.patterns[type]) {
console.error(`Validatore "${type}" non trovato.`);
return false;
}
return this.patterns[type].test(value);
},
// Funzione di validazione con feedback (per UI)
validatePassword(password) {
const errors = []; // Un accumulatore di errori!
if (password.length < 8) {
errors.push("Deve contenere almeno 8 caratteri");
}
if (!/[A-Z]/.test(password)) {
errors.push("Deve contenere almeno una lettera maiuscola");
}
if (!/[a-z]/.test(password)) {
errors.push("Deve contenere almeno una lettera minuscola");
}
if (!/\d/.test(password)) {
errors.push("Deve contenere almeno un numero");
}
return {
valid: errors.length === 0,
errors: errors // Restituisce la lista di problemi
};
}
};
// --- Esempio di utilizzo ---
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);
// ["Deve contenere almeno 8 caratteri", "Deve contenere almeno una lettera maiuscola", ...]
43. Pattern di Gestione Stato
La Gestione dello Stato (State Management) è come dirigere un'orchestra. Lo "Stato" è la partitura musicale (i dati: chi è loggato, il punteggio, gli articoli nel carrello). Se ogni musicista (componente UI) ha la sua versione leggermente diversa della partitura (dati duplicati o non sincronizzati), il risultato sarà il caos.
Questi pattern servono a garantire che tutti stiano suonando dallo stesso identico spartito.
Il Pattern "Single Source of Truth" (SSOT)
Questo è il principio più importante: lo stato della tua applicazione deve vivere in un unico posto.
Analogo: Invece di avere appunti sparsi su post-it per tutto l'ufficio (un userName nel header, un altro userName nel profilo, una listaTask qui e una là), hai un unico "libro mastro" centrale (o una lavagna principale) a cui tutti fanno riferimento.
Il Problema (Senza SSOT):
- Un componente
Headerha una variabileuserName = "Mario". - Un componente
ProfilePageha un'altra variabileuserName = "Mario". - L'utente aggiorna il suo nome in "Luigi" nella
ProfilePage. - La
ProfilePageaggiorna la sua variabile. - RISULTATO (BUG): La
ProfilePageora dice "Luigi", ma l'Headerdice ancora "Mario". I dati non sono sincronizzati.
La Soluzione (Con SSOT):
Esiste un unico oggetto StateManager (il "libro mastro").
// Un "libro mastro" centralizzato (Single Source of Truth)
const StateManager = {
// 1. Stato privato (il vero libro mastro)
_state: {
user: { nome: "Mario" },
tasks: [],
settings: { theme: 'light' }
},
// 2. Un "cancello" per LEGGERE (ritorna sempre una copia)
getState() {
// Ritorna una copia per evitare modifiche accidentali
return JSON.parse(JSON.stringify(this._state));
},
// 3. Un "cancello" per SCRIVERE (l'unico modo per cambiare)
setState(path, value) {
// Es. path = "user.nome", value = "Luigi"
const keys = path.split('.');
let target = this._state;
// Naviga l'oggetto per trovare dove scrivere
for (let i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
// 4. Notifica tutti che qualcosa è cambiato!
this._notify(path, value);
},
// Sistema di notifica (vedi "Event-Driven")
_listeners: [],
subscribe(callback) {
this._listeners.push(callback);
},
_notify(path, value) {
this._listeners.forEach(cb => cb(path, value));
}
};
- Come funziona ora:
HeadereProfilePageleggono entrambi daStateManager.getState().- L'utente cambia nome.
ProfilePagechiamaStateManager.setState("user.nome", "Luigi"). - Lo
StateManageraggiorna il suo stato e_notify()allerta tutti i "sottoscrittori". - L'
Header, essendo un sottoscrittore, riceve la notifica e aggiorna la sua UI. - RISULTATO: Tutta l'app è perfettamente sincronizzata.
Pattern CRUD per Liste
CRUD è un acronimo che descrive le quattro operazioni fondamentali per gestire qualsiasi collezione di dati (come una lista di task, un carrello della spesa, una lista di utenti).
- Create (Creare)
- Read (Leggere)
- Update (Aggiornare)
- Delete (Eliminare)
Analogo: È come gestire una biblioteca di libri (task).
Creare una classe (come un TaskManager) è il modo più pulito per incapsulare questa logica, combinando lo stato (SSOT) con i metodi per manipolarlo.
class TaskManager {
constructor() {
// SSOT: 'this.tasks' è l'unica fonte di verità per i task
this.tasks = [];
this.loadTasksFromStorage(); // Carica dati salvati
}
// --- CREATE ---
// (Aggiungere un nuovo libro alla biblioteca)
addTask(text) {
const newTask = {
id: `task-${Date.now()}`, // ID unico
text: text,
completed: false,
createdAt: new Date().toISOString()
};
this.tasks.push(newTask);
this.save(); // Salva dopo ogni modifica
this.render(); // Aggiorna l'UI
return newTask;
}
// --- READ ---
// (Trovare libri nella biblioteca)
getTask(id) {
return this.tasks.find(task => task.id === id);
}
getAllTasks() {
return [...this.tasks]; // Ritorna una *copia* (Immutabilità!)
}
getFilteredTasks(filter) { // Es. 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 ---
// (Cambiare la copertina o il titolo di un libro)
updateTask(id, updates) { // 'updates' è un oggetto, es. { text: "Nuovo testo" }
const index = this.tasks.findIndex(task => task.id === id);
if (index === -1) return false; // Non trovato
// Unisci il vecchio task con le nuove modifiche
this.tasks[index] = {
...this.tasks[index], // Vecchi dati
...updates, // Nuovi dati (sovrascrivono)
updatedAt: new Date().toISOString()
};
this.save();
this.render();
return this.tasks[index];
}
// Metodo helper per un update comune
toggleTask(id) {
const task = this.getTask(id);
if (task) {
this.updateTask(id, { completed: !task.completed });
}
}
// --- DELETE ---
// (Rimuovere un libro dalla biblioteca)
deleteTask(id) {
const index = this.tasks.findIndex(task => task.id === id);
if (index === -1) return false;
this.tasks.splice(index, 1); // .splice() modifica l'array originale
this.save();
this.render();
return true;
}
// --- METODI DI SUPPORTO ---
save() {
// Usa i pattern di localStorage (Parte III, capitolo 29)
localStorage.setItem('tasks', JSON.stringify(this.tasks));
}
loadTasksFromStorage() {
// Usa il pattern "Gestire il Primo Avvio"
this.tasks = JSON.parse(localStorage.getItem('tasks')) || [];
}
render() {
// Logica per aggiornare il DOM (Parte III)
console.log("Aggiorno l'interfaccia con i nuovi task...", this.tasks);
}
}
Pattern di Reset e Stato Iniziale
Come gestisci il "reset di fabbrica" della tua applicazione? (Es. quando un utente fa il logout, o inizia una "Nuova Partita").
Il Problema: Potresti essere tentato di resettare manualmente ogni pezzo dello stato.
// CATTIVO: Facile dimenticare qualcosa 👎
function resetApp() {
StateManager.state.user = null;
StateManager.state.tasks = [];
StateManager.state.settings.theme = 'light';
// Ops! Ho dimenticato di resettare state.ui.isLoading!
}
La Soluzione (Pattern: Stato Iniziale come Funzione): Definisci il tuo stato iniziale non come un oggetto statico, ma come una funzione che restituisce un nuovo oggetto.
Analogo: Invece di avere un singolo "modulo di iscrizione" originale che tutti scarabocchiano (un oggetto), hai una pila di moduli nuovi e puliti (una funzione). Per resettare, butti via il modulo scarabocchiato e ne prendi uno nuovo dalla pila.
// 1. Definisci la "fabbrica" di stato iniziale
const createInitialState = () => ({
form: {
nome: '',
email: '',
messaggio: ''
},
ui: {
isSubmitting: false,
errors: [],
successMessage: null
},
data: []
});
// 2. Il tuo manager usa la fabbrica per iniziare
const FormManager = {
state: createInitialState(), // Crea il primo "modulo"
// 3. Il reset ora è pulito, sicuro e completo!
reset() {
// Butta via il vecchio stato e ne prende uno nuovo di fabbrica
this.state = createInitialState();
this.render(); // Aggiorna l'UI
},
// ...altri metodi...
render() {
console.log("Renderizzo lo stato...", this.state);
}
};
// Uso:
FormManager.state.form.nome = "Mario"; // Modifico lo stato
FormManager.reset(); // Resettato!
Perché una funzione e non un oggetto const?
Se createInitialState fosse un oggetto const, quando lo assegni a state (this.state = initialState), staresti assegnando un riferimento (un "collegamento"). Se modificassi this.state.form.nome, staresti modificando anche l'originale initialState! La funzione garantisce che tu ottenga sempre una copia fresca e pulita.