Funzioni Avanzate

Arrow functions, closures, callback, IIFE e programmazione funzionale

🚀 Funzioni JavaScript Moderne

In questa lezione esploriamo le funzionalità avanzate delle funzioni JavaScript:
sintassi moderna, pattern di programmazione funzionale e concetti potenti come closures e callback.

🎯 In Questa Lezione

  • Arrow Functions – Sintassi ES6 compatta
  • Closures – Funzioni che “ricordano” lo scope
  • Callback – Funzioni passate come argomenti
  • Higher-Order Functions – Funzioni che usano funzioni
  • IIFE – Funzioni auto-eseguibili

➡️ Arrow Functions (ES6)

Le arrow functions sono una sintassi più breve per scrivere funzioni,
introdotte in ES6 (2015). Molto usate nella programmazione moderna!

1
Sintassi Base

Diversi modi di scrivere arrow functions.

Arrow Function Syntax
// Funzione classica
function somma(a, b) {
    return a + b;
}

// ✨ Arrow function - forma completa
const sommaArrow = (a, b) => {
    return a + b;
};

// ✨ Arrow function - forma breve (return implicito)
const sommaBreve = (a, b) => a + b;

// Test
console.log(somma(5, 3));        // 8
console.log(sommaArrow(5, 3));   // 8
console.log(sommaBreve(5, 3));   // 8

// Un solo parametro - parentesi opzionali
const quadrato = n => n * n;
console.log(quadrato(5));  // 25

// Nessun parametro - parentesi obbligatorie
const saluta = () => "Ciao!";
console.log(saluta());  // "Ciao!"

2
Return Implicito

Se la funzione ha una sola espressione, il return è automatico!

Return Implicito
// ✅ Con return implicito (consigliato se breve)
const doppio = n => n * 2;
const isPari = n => n % 2 === 0;
const max = (a, b) => a > b ? a : b;

// Return oggetto - servono parentesi tonde!
const creaUtente = (nome, età) => ({
    nome: nome,
    età: età,
    adulto: età >= 18
});

console.log(doppio(5));           // 10
console.log(isPari(4));           // true
console.log(max(10, 5));          // 10
console.log(creaUtente("Mario", 25));
// {nome: "Mario", età: 25, adulto: true}

// ❌ Senza parentesi tonde per oggetto = ERRORE
// const errore = (nome) => {nome: nome};  // Pensa sia un blocco!

3
Differenze con Funzioni Classiche

Arrow functions si comportano diversamente con this.

this Binding
// Funzione classica - 'this' dipende dal contesto
const persona1 = {
    nome: "Mario",
    saluta: function() {
        console.log(`Ciao, sono ${this.nome}`);
    }
};

persona1.saluta();  // "Ciao, sono Mario"

// Arrow function - 'this' è lessicale (eredita dallo scope)
const persona2 = {
    nome: "Anna",
    saluta: () => {
        console.log(`Ciao, sono ${this.nome}`);  // 'this' non è persona2!
    }
};

persona2.saluta();  // "Ciao, sono undefined"

// ⚠️ Arrow functions NON vanno bene come metodi di oggetti!

📊 Quando Usare Arrow Functions

Situazione Usa Arrow Motivo
Callback (map, filter…) ✅ Sì Sintassi più breve e pulita
Funzioni brevi ✅ Sì Return implicito comodo
Metodi oggetto ❌ No Problema con ‘this’
Costruttori ❌ No Non hanno ‘new’

💡 Best Practice Arrow Functions
  • ✅ Usa per callback e funzioni brevi
  • ✅ Preferisci return implicito quando possibile
  • ❌ Evita per metodi di oggetti
  • ❌ Non usare se serve this, arguments, o new

🔐 Closures

Una closure è una funzione che “ricorda” le variabili del suo scope esterno,
anche dopo che la funzione esterna è terminata. Concetto potente e fondamentale!

1
Closure Base

La funzione interna accede alle variabili della funzione esterna.

Closure Semplice
function creaContatore() {
    let count = 0;  // Variabile "privata"
    
    return function() {
        count++;
        return count;
    };
}

const contatore = creaContatore();

console.log(contatore());  // 1
console.log(contatore());  // 2
console.log(contatore());  // 3

// Ogni closure mantiene il SUO stato
const contatore2 = creaContatore();
console.log(contatore2());  // 1 (nuovo contatore!)
console.log(contatore());   // 4 (primo contatore continua!)

// 'count' non è accessibile dall'esterno
// console.log(count);  // ❌ ERRORE!

2
Variabili Private con Closures

Simula l’incapsulamento creando variabili “private”.

Variabili Private
function creaBancaAccount(saldoIniziale) {
    let saldo = saldoIniziale;  // Privato!
    
    return {
        deposita: function(importo) {
            if (importo > 0) {
                saldo += importo;
                console.log(`Depositati €${importo}. Saldo: €${saldo}`);
            }
        },
        
        preleva: function(importo) {
            if (importo > 0 && importo <= saldo) {
                saldo -= importo;
                console.log(`Prelevati €${importo}. Saldo: €${saldo}`);
            } else {
                console.log("Fondi insufficienti!");
            }
        },
        
        getSaldo: function() {
            return saldo;
        }
    };
}

const account = creaBancaAccount(1000);

account.deposita(500);      // Depositati €500. Saldo: €1500
account.preleva(200);       // Prelevati €200. Saldo: €1300
console.log(account.getSaldo());  // 1300

// ❌ Non posso accedere direttamente a 'saldo'
// console.log(account.saldo);  // undefined
// account.saldo = 999999;      // Non funziona!

3
Factory Functions con Closures

Crea funzioni personalizzate usando closures.

Factory Functions
// Factory: crea funzioni moltiplicatore
function creaMoltiplicatore(fattore) {
    return function(numero) {
        return numero * fattore;
    };
}

const doppio = creaMoltiplicatore(2);
const triplo = creaMoltiplicatore(3);
const decuplo = creaMoltiplicatore(10);

console.log(doppio(5));   // 10
console.log(triplo(5));   // 15
console.log(decuplo(5));  // 50

// Factory: crea funzioni saluto personalizzate
function creaSaluto(saluto) {
    return nome => `${saluto}, ${nome}!`;
}

const salutaItaliano = creaSaluto("Ciao");
const salutaInglese = creaSaluto("Hello");
const salutaSpagnolo = creaSaluto("Hola");

console.log(salutaItaliano("Mario"));   // "Ciao, Mario!"
console.log(salutaInglese("John"));     // "Hello, John!"
console.log(salutaSpagnolo("Carlos"));  // "Hola, Carlos!"

💡 Come Funzionano le Closures

Quando una funzione viene creata, porta con sé una "valigia"
con tutte le variabili dello scope in cui è stata definita. Questa valigia
resta accessibile anche dopo che la funzione esterna è terminata!

🔄 Callback Functions

Un callback è una funzione passata come argomento a un'altra funzione
e eseguita successivamente. Pattern fondamentale in JavaScript!

1
Callback Base

Passa funzioni come argomenti.

Callback Semplice
// Funzione che accetta un callback
function eseguiOperazione(a, b, operazione) {
    return operazione(a, b);
}

// Callback diverse
function somma(x, y) {
    return x + y;
}

function moltiplica(x, y) {
    return x * y;
}

// Uso
console.log(eseguiOperazione(5, 3, somma));        // 8
console.log(eseguiOperazione(5, 3, moltiplica));   // 15

// Con arrow function inline
console.log(eseguiOperazione(10, 2, (a, b) => a - b));  // 8
console.log(eseguiOperazione(10, 2, (a, b) => a / b));  // 5

2
Callback Asincroni

Molto usati con operazioni asincrone (timer, API...).

Callback Asincroni
// setTimeout - esegue callback dopo delay
console.log("Inizio");

setTimeout(function() {
    console.log("Dopo 2 secondi");
}, 2000);

console.log("Fine");

// Output:
// Inizio
// Fine
// (dopo 2 secondi) Dopo 2 secondi

// Simulazione caricamento dati
function caricaDati(callback) {
    console.log("Caricamento in corso...");
    
    setTimeout(() => {
        const dati = {
            utente: "Mario",
            età: 30,
            email: "mario@example.com"
        };
        
        callback(dati);
    }, 1000);
}

// Uso
caricaDati(function(dati) {
    console.log("Dati caricati:", dati);
});

// Con arrow function
caricaDati(dati => {
    console.log(`Utente: ${dati.utente}, Età: ${dati.età}`);
});

3
Callback con Array Methods

forEach, map, filter, reduce usano callback.

Array Callbacks
const numeri = [1, 2, 3, 4, 5];

// forEach - esegue callback per ogni elemento
numeri.forEach(function(numero) {
    console.log(numero * 2);
});

// map - trasforma array
const doppi = numeri.map(n => n * 2);
console.log(doppi);  // [2, 4, 6, 8, 10]

// filter - filtra elementi
const pari = numeri.filter(n => n % 2 === 0);
console.log(pari);  // [2, 4]

// Callback con indice
const frutti = ["mela", "banana", "arancia"];
frutti.forEach((frutto, indice) => {
    console.log(`${indice + 1}. ${frutto}`);
});
// 1. mela
// 2. banana
// 3. arancia

⚠️ Callback Hell

Troppi callback annidati rendono il codice difficile da leggere.
Questo problema si risolve con Promises e async/await
(Lezione 16).

// ❌ Callback Hell (piramide del destino)
step1(function(result1) {
    step2(result1, function(result2) {
        step3(result2, function(result3) {
            step4(result3, function(result4) {
                console.log(result4);
            });
        });
    });
});

// ✅ Meglio con Promises (Lezione 16)
step1()
    .then(result1 => step2(result1))
    .then(result2 => step3(result2))
    .then(result3 => step4(result3))
    .then(result4 => console.log(result4));

🎯 Higher-Order Functions

Una higher-order function è una funzione che:

  • Accetta funzioni come parametri, O
  • Restituisce una funzione, O
  • Entrambe!
1
Funzioni che Accettano Funzioni

Higher-Order: Accettano Funzioni
// Ripeti un'operazione n volte
function ripeti(n, azione) {
    for (let i = 0; i < n; i++) {
        azione(i);
    }
}

// Uso
ripeti(3, i => console.log(`Iterazione ${i}`));
// Iterazione 0
// Iterazione 1
// Iterazione 2

// Applica funzione a ogni elemento array
function map(arr, trasforma) {
    let risultato = [];
    for (let elemento of arr) {
        risultato.push(trasforma(elemento));
    }
    return risultato;
}

const numeri = [1, 2, 3, 4, 5];
const quadrati = map(numeri, n => n * n);
console.log(quadrati);  // [1, 4, 9, 16, 25]

2
Funzioni che Restituiscono Funzioni

Higher-Order: Restituiscono Funzioni
// Crea funzione checker
function creaChecker(limite) {
    return function(valore) {
        return valore > limite;
    };
}

const maggioreDi10 = creaChecker(10);
const maggioreDi100 = creaChecker(100);

console.log(maggioreDi10(15));   // true
console.log(maggioreDi10(5));    // false
console.log(maggioreDi100(150)); // true

// Composizione funzioni
function componi(f, g) {
    return function(x) {
        return f(g(x));
    };
}

const aggiungi5 = x => x + 5;
const moltiplica2 = x => x * 2;

const aggiungiEMoltiplica = componi(moltiplica2, aggiungi5);
console.log(aggiungiEMoltiplica(10));  // (10 + 5) * 2 = 30

3
Currying

Trasforma funzione multi-parametro in catena di funzioni singolo parametro.

Currying
// Funzione normale
function somma(a, b, c) {
    return a + b + c;
}

console.log(somma(1, 2, 3));  // 6

// ✨ Versione curried
function sommaCurried(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

console.log(sommaCurried(1)(2)(3));  // 6

// Con arrow functions (più compatto)
const sommaCurriedArrow = a => b => c => a + b + c;
console.log(sommaCurriedArrow(1)(2)(3));  // 6

// Uso pratico: partial application
const aggiungi10 = sommaCurriedArrow(10);
const aggiungi10E5 = aggiungi10(5);

console.log(aggiungi10E5(3));  // 18
console.log(aggiungi10E5(7));  // 22

⚡ IIFE (Immediately Invoked Function Expression)

Un IIFE è una funzione che viene eseguita immediatamente dopo essere stata definita.
Utile per creare scope isolato.

1
Sintassi IIFE

IIFE Base
// Funzione normale
function saluta() {
    console.log("Ciao!");
}
saluta();  // Devi chiamarla

// ✨ IIFE - eseguita subito
(function() {
    console.log("Ciao da IIFE!");
})();

// IIFE con parametri
(function(nome) {
    console.log(`Ciao ${nome}!`);
})("Mario");

// IIFE con arrow function
(() => {
    console.log("IIFE arrow!");
})();

// IIFE che restituisce valore
const risultato = (function() {
    return 10 + 5;
})();

console.log(risultato);  // 15

2
Perché Usare IIFE

Crea uno scope privato, evita inquinamento globale.

IIFE per Privacy
// ❌ Senza IIFE - variabili globali
var counter = 0;
function increment() {
    counter++;
}
// 'counter' è globale - può essere modificato da chiunque

// ✅ Con IIFE - scope privato
const counterModule = (function() {
    let count = 0;  // Privato!
    
    return {
        increment: function() {
            count++;
            console.log(count);
        },
        reset: function() {
            count = 0;
        },
        getCount: function() {
            return count;
        }
    };
})();

counterModule.increment();  // 1
counterModule.increment();  // 2
console.log(counterModule.getCount());  // 2
// console.log(count);  // ❌ ERRORE! Non accessibile

💡 IIFE Oggi

Con ES6 modules, IIFE sono meno usati per l'isolamento dello scope.
Tuttavia, restano utili per inizializzazioni immediate e pattern legacy.

🚀 Esempio Completo: Task Manager

Task Manager con Funzioni Avanzate
// Task Manager usando tutti i concetti
const TaskManager = (function() {
    // Closure: stato privato
    let tasks = [];
    let nextId = 1;
    
    // Higher-order: filtra tasks
    const filterTasks = (predicate) => tasks.filter(predicate);
    
    return {
        // Aggiungi task
        addTask: (description, priority = "media") => {
            const task = {
                id: nextId++,
                description,
                priority,
                completed: false,
                createdAt: new Date()
            };
            tasks.push(task);
            console.log(`✅ Task aggiunto: "${description}"`);
            return task;
        },
        
        // Completa task (callback)
        completeTask: (id, callback) => {
            const task = tasks.find(t => t.id === id);
            if (task) {
                task.completed = true;
                console.log(`✔️  Task completato: "${task.description}"`);
                if (callback) callback(task);
            } else {
                console.log("❌ Task non trovato");
            }
        },
        
        // Lista tasks (arrow function + higher-order)
        getTasks: () => tasks.slice(),
        
        getCompletedTasks: () => filterTasks(t => t.completed),
        
        getPendingTasks: () => filterTasks(t => !t.completed),
        
        getByPriority: (priority) => filterTasks(t => t.priority === priority),
        
        // Esegui azione su ogni task (callback)
        forEach: (action) => {
            tasks.forEach(action);
        },
        
        // Map tasks (higher-order)
        map: (transform) => tasks.map(transform),
        
        // Stats (closure + metodi)
        getStats: () => ({
            total: tasks.length,
            completed: filterTasks(t => t.completed).length,
            pending: filterTasks(t => !t.completed).length,
            byPriority: {
                alta: filterTasks(t => t.priority === "alta").length,
                media: filterTasks(t => t.priority === "media").length,
                bassa: filterTasks(t => t.priority === "bassa").length
            }
        }),
        
        // Clear (con conferma callback)
        clear: (onConfirm) => {
            if (onConfirm && onConfirm()) {
                tasks = [];
                console.log("🗑️  Tutti i task eliminati");
            }
        }
    };
})();

// === USO DEL TASK MANAGER ===

// Aggiungi tasks
TaskManager.addTask("Studiare JavaScript", "alta");
TaskManager.addTask("Fare la spesa", "media");
TaskManager.addTask("Chiamare Mario", "bassa");
TaskManager.addTask("Completare progetto", "alta");

// Completa con callback
TaskManager.completeTask(1, task => {
    console.log(`Completato alle: ${new Date().toLocaleTimeString()}`);
});

TaskManager.completeTask(3);

// Filtra per priorità
console.log("\n📌 Task ad alta priorità:");
TaskManager.getByPriority("alta").forEach(t => {
    console.log(`  - ${t.description} ${t.completed ? "✔️" : "⏳"}`);
});

// Map: estrai solo descrizioni
const descrizioni = TaskManager.map(t => t.description);
console.log("\n📝 Tutte le descrizioni:", descrizioni);

// Stats
const stats = TaskManager.getStats();
console.log("\n📊 Statistiche:");
console.log(`  Totale: ${stats.total}`);
console.log(`  Completati: ${stats.completed}`);
console.log(`  Pendenti: ${stats.pending}`);
console.log(`  Alta priorità: ${stats.byPriority.alta}`);

// ForEach con arrow function
console.log("\n📋 Lista completa:");
TaskManager.forEach(t => {
    const status = t.completed ? "✅" : "⏳";
    const priority = t.priority.toUpperCase();
    console.log(`  ${status} [${priority}] ${t.description}`);
});

// Output:
// ✅ Task aggiunto: "Studiare JavaScript"
// ✅ Task aggiunto: "Fare la spesa"
// ✅ Task aggiunto: "Chiamare Mario"
// ✅ Task aggiunto: "Completare progetto"
// ✔️  Task completato: "Studiare JavaScript"
// Completato alle: 14:30:45
// ✔️  Task completato: "Chiamare Mario"
// 
// 📌 Task ad alta priorità:
//   - Studiare JavaScript ✔️
//   - Completare progetto ⏳
// 
// 📝 Tutte le descrizioni: ["Studiare JavaScript", "Fare la spesa", "Chiamare Mario", "Completare progetto"]
// 
// 📊 Statistiche:
//   Totale: 4
//   Completati: 2
//   Pendenti: 2
//   Alta priorità: 2
// 
// 📋 Lista completa:
//   ✅ [ALTA] Studiare JavaScript
//   ⏳ [MEDIA] Fare la spesa
//   ✅ [BASSA] Chiamare Mario
//   ⏳ [ALTA] Completare progetto

💡 Concetti Usati nell'Esempio
  • IIFE - Modulo TaskManager auto-eseguibile
  • Closure - `tasks` e `nextId` privati
  • Arrow Functions - Sintassi moderna compatta
  • Callback - completeTask, forEach, clear
  • Higher-Order - filter, map, forEach
  • Return implicito - Arrow functions brevi

💪 Esercizi Pratici

📝 Esercizio 1: Debounce Function

Obiettivo: Crea funzione debounce usando closures

  • Debounce ritarda l'esecuzione di una funzione
  • Se chiamata di nuovo prima del delay, resetta il timer
  • Utile per search input, resize events...

📝 Esercizio 2: Pipeline Function

Obiettivo: Crea funzione che compone più funzioni

  • Accetta array di funzioni
  • Restituisce funzione che le esegue in sequenza
  • Output di una = input della successiva

📝 Esercizio 3: Memoization

Obiettivo: Ottimizza funzioni costose con cache

  • Crea funzione memoize che crea cache risultati
  • Se input già calcolato, restituisce dalla cache
  • Testa con funzione Fibonacci

🎯 Riepilogo Lezione

Concetto Sintassi Uso
Arrow Function () => {} Sintassi breve, callback
Closure Funzione in funzione Privacy, factory, memoization
Callback Funzione come arg Eventi, async, array methods
Higher-Order Riceve/restituisce fn Composizione, utilities
IIFE (function(){})() Scope isolato, init

Lezione 10 del Corso JavaScript Completo

Torna in alto