Types · Operations · Scope · Control Flow · Functions · this · ES6 Classes
JavaScript ha 7 tipi primitivi (immutabili, passati per valore) e poi gli oggetti (passati per riferimento). Conoscere la distinzione evita bug sottili.
let a = "ciao"; let b = a; b riceve una copia indipendente. Gli oggetti invece condividono il riferimento: modificare uno modifica anche l'altro. Capire questo previene la maggior parte dei bug "perché è cambiato anche quello?".typeof 42 // "number"
typeof "ciao" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ← storico bug di JS
typeof {} // "object"
typeof [] // "object" ← array è un oggetto!
typeof function(){} // "function"
JS converte automaticamente i tipi in molte operazioni. Questo è sia una comodità che una fonte di sorprese.
"5" + 3 // "53" → + con stringa = concatenazione
"5" - 3 // 2 → - forza conversione numerica
true + 1 // 2 → true vale 1
false + 1 // 1
null + 1 // 1 → null vale 0 in contesto numerico
"" == false // true ← == fa coercizione, evitarlo!
"" === false// false ← === non fa coercizione, usare sempre
=== invece di ==. Il doppio uguale applica coercizione implicita, il triplo no. L'unica eccezione accettata è x == null che cattura sia null che undefined.| Operatore | Significato |
|---|---|
| + - * / % | aritmetica (% = resto) |
| ** | potenza: 2**10 = 1024 |
| === !== | uguaglianza/disuguaglianza stretta |
| < > <= >= | confronto |
| && || ! | AND, OR, NOT logici |
| ?? | nullish coalescing: a ?? b → b se a è null/undefined |
| ?. | optional chaining: obj?.prop (non lancia se obj è null) |
| ++ -- | incremento/decremento |
| += -= *= | assegnamento composto |
let a = 10, b = 3;
a % b // 1 (10 mod 3)
a ** b // 1000
let user = null;
user?.name // undefined — non lancia TypeError
let val = user ?? "ospite"; // "ospite"
// short-circuit: && si ferma al primo falsy, || al primo truthy
true && console.log("eseguito");
false && console.log("mai");
?? e ?. (ES2020) risolvono un problema classico: prima si scriveva user && user.name. Il nullish coalescing è più preciso di || perché || si attiva anche su 0, "", false (falsy ma validi), mentre ?? solo su null/undefined.Lo scope è la regione di codice in cui una variabile è visibile. JavaScript ha tre parole chiave di dichiarazione, con comportamenti profondamente diversi.
| scope | hoisting | re-assegnabile | re-dichiarabile | |
|---|---|---|---|---|
| var | funzione | sì (→ undefined) | sì | sì |
| let | blocco {} | TDZ* | sì | no |
| const | blocco {} | TDZ* | no (binding) | no |
* TDZ = Temporal Dead Zone: la variabile esiste ma accedervi prima della dichiarazione lancia ReferenceError.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// stampa: 3 3 3 ← var sfugge dal blocco for!
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// stampa: 0 1 2 ← let crea una nuova binding per ogni iterazione
console.log(x); // undefined — NON ReferenceError!
var x = 5;
// var viene "issata" in cima alla funzione come: var x; (vale undefined)
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5; // TDZ: l'errore è esplicito e utile
const obj = { x: 1 };
obj.x = 99; // OK — il contenuto è mutabile
obj = {}; // TypeError — il binding è costante
const arr = [1,2];
arr.push(3); // OK
var è rimasto per retrocompatibilità. let e const (ES6, 2015) correggono i suoi difetti. La regola pratica moderna: usa sempre const di default; passa a let solo se la variabile va riassegnata. Non usare mai var in codice nuovo.Il ramo condizionale fondamentale. La condizione viene valutata come truthy/falsy, non necessariamente come booleano esatto.
if (condizione) {
// eseguito se truthy
} else if (altraCondizione) {
// eseguito se la prima è falsy e questa è truthy
} else {
// eseguito se tutte le precedenti sono falsy
}
// Questi sono falsy — tutto il resto è truthy:
false 0 -0 0n "" null undefined NaN
// Truthy (anche questi sorprendono i principianti):
"0" // stringa non-vuota è truthy!
[] // array vuoto è truthy!
{} // oggetto vuoto è truthy!
const msg = age >= 18 ? "adulto" : "minore";
// equivale a:
let msg;
if (age >= 18) msg = "adulto";
else msg = "minore";
Confronto con uguaglianza stretta (===) su un singolo valore contro più casi. Utile quando hai molti rami su uno stesso valore discreto.
switch (giorno) {
case "lunedì":
case "martedì":
console.log("inizio settimana");
break; // ← FONDAMENTALE: senza break, cade nel case dopo
case "venerdì":
console.log("TGIF");
break;
default:
console.log("metà settimana");
}
switch (livello) {
case "admin":
grantAdmin(); // fall-through voluto:
case "user":
grantAccess(); // admin esegue ENTRAMBI
break;
}
break) è la trappola più comune di switch. Molti team usano un commento esplicito // fallthrough per indicare che è intenzionale, così il code review non confonde con un bug.const actions = { "a": fn1, "b": fn2 }; actions[key]?.();while esegue il corpo finché la condizione è truthy. Adatto quando non conosci a priori il numero di iterazioni.
let n = 1;
while (n < 100) {
n *= 2;
}
// n = 128 (prima potenza di 2 ≥ 100)
Esegue il corpo almeno una volta, poi controlla la condizione.
let input;
do {
input = prompt("Inserisci un numero positivo:");
} while (input <= 0); // ripete se non valido
let i = 0;
while (true) {
i++;
if (i % 2 === 0) continue; // salta i pari
if (i > 9) break; // esce dal loop
console.log(i); // 1 3 5 7 9
}
for (let i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
// init; condizione; aggiornamento
const colori = ["rosso", "verde", "blu"];
for (const c of colori) {
console.log(c);
}
// con indice:
for (const [i, c] of colori.entries()) {
console.log(i, c);
}
const punto = { x: 1, y: 2, z: 3 };
for (const k in punto) {
console.log(k, punto[k]); // x 1 / y 2 / z 3
}
for…in sugli array: itera anche proprietà ereditate dal prototipo, e l'ordine non è garantito. Per gli array, usa sempre for…of o i metodi .forEach / .map / .filter.const nums = [1,2,3,4,5];
nums.forEach(n => console.log(n)); // effetti collaterali
const doppi = nums.map(n => n * 2); // [2,4,6,8,10]
const pari = nums.filter(n => n % 2 === 0); // [2,4]
const somma = nums.reduce((acc,n) => acc+n, 0); // 15
In JS le funzioni sono first-class citizens: possono essere assegnate a variabili, passate come argomenti, restituite da altre funzioni.
saluta("Dario"); // OK — hoisted, usabile prima della dichiarazione
function saluta(nome) {
return `Ciao, ${nome}!`;
}
const somma = function(a, b) {
return a + b;
};
const quadrato = x => x * x; // corpo singolo → return implicito
const add = (a, b) => a + b;
const multiline = (a, b) => {
const r = a + b;
return r;
};
// default value
function power(base, exp = 2) { return base ** exp; }
// rest params (raccoglie argomenti extra in array)
function sum(...nums) {
return nums.reduce((a,b) => a+b, 0);
}
sum(1, 2, 3, 4); // 10
// destructuring nei parametri
function greet({ name, lang = "it" }) {
return lang === "it" ? `Ciao ${name}` : `Hello ${name}`;
}
function counter() {
let n = 0;
return () => ++n; // la funzione interna "chiude" su n
}
const c1 = counter();
c1(); // 1
c1(); // 2 — n persiste tra le chiamate, ma è privato
this è forse il concetto più confuso di JS. Non dipende da dove la funzione è definita, ma da come viene chiamata (tranne le arrow function).
| Contesto di chiamata | this vale |
|---|---|
| funzione libera (strict) | undefined |
| funzione libera (non-strict) | window / global |
| metodo di oggetto: obj.f() | obj |
| new F() | nuovo oggetto creato |
| f.call(x, a, b) | x (esplicito) |
| f.apply(x, [a,b]) | x (esplicito) |
| f.bind(x)(…) | x (legato permanentemente) |
| arrow function | this del contesto lessicale |
const persona = {
nome: "Dario",
saluta() {
console.log(this.nome); // "Dario" — this = persona
}
};
persona.saluta();
const fn = persona.saluta;
fn(); // undefined (strict) — this è perso!
function Timer() {
this.secondi = 0;
setInterval(function() {
this.secondi++; // BUG: this è undefined/global!
}, 1000);
}
function TimerOK() {
this.secondi = 0;
setInterval(() => {
this.secondi++; // OK: arrow eredita this da TimerOK
}, 1000);
}
function introduce(saluto) {
return `${saluto}, sono ${this.nome}`;
}
const p = { nome: "Dario" };
introduce.call(p, "Ciao"); // "Ciao, sono Dario"
introduce.apply(p, ["Salve"]); // "Salve, sono Dario"
const bound = introduce.bind(p);
bound("Ehi"); // "Ehi, sono Dario"
La sintassi class è zucchero sintattico sul sistema a prototipi di JS. Non introduce un nuovo modello OOP: dietro le quinte usa ancora prototype. Ma rende il codice molto più leggibile e gestibile.
class Animale {
// constructor: chiamato da new Animale(...)
constructor(nome, verso) {
this.nome = nome;
this.verso = verso;
}
// metodo (sulla prototype chain, non duplicato per ogni istanza)
parla() {
return `${this.nome} dice ${this.verso}`;
}
// getter/setter
get info() { return `Animale: ${this.nome}`; }
// metodo statico (su classe, non sull'istanza)
static crea(nome) { return new Animale(nome, "..."); }
}
class Cane extends Animale {
constructor(nome, razza) {
super(nome, "Woof"); // super() chiama il costruttore padre
this.razza = razza;
}
parla() {
return super.parla() + " (da cane)"; // super.metodo()
}
}
const fido = new Cane("Fido", "Labrador");
fido.parla(); // "Fido dice Woof (da cane)"
fido.info; // "Animale: Fido"
fido instanceof Cane; // true
fido instanceof Animale; // true
class Conto {
#saldo = 0; // campo privato (# prefisso)
deposita(n) { this.#saldo += n; }
get saldo() { return this.#saldo; }
}
const c = new Conto();
c.deposita(100);
c.saldo; // 100
c.#saldo; // SyntaxError — davvero privato
.prototype. La sintassi class non cambia la semantica (è ancora prototipale), ma riduce il boilerplate e rende espliciti constructor, ereditarietà, override — concetti che prima erano impliciti e fragili. I campi privati con # (ES2022) finalmente risolvono l'incapsulamento reale, sostituendo la convenzione informale del prefisso _.