Vai al contenuto

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!"
  1. 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!
  1. 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!
  1. 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!
  1. 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
  1. 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à}`);
});
  1. 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 (Promise e async/await).

// ❌ 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 le Promise (vedi la lezione Promise e async/await)
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]
  1. 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
  1. 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
  1. 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...
Mostra la soluzione

Soluzione Esercizio 1

function debounce(func, delay) {
    let timeoutId;  // Closure: mantiene riferimento al timer

    return function(...args) {
        // Cancella timer precedente
        clearTimeout(timeoutId);

        // Imposta nuovo timer
        timeoutId = setTimeout(() => {
            func(...args);
        }, delay);
    };
}

// Test
const salutaConDelay = debounce((nome) => {
    console.log(`Ciao ${nome}!`);
}, 1000);

// Chiamate rapide
salutaConDelay("Mario");   // Cancellata
salutaConDelay("Anna");    // Cancellata
salutaConDelay("Luca");    // Questa viene eseguita dopo 1 sec

// Uso pratico: search input
const search = debounce((query) => {
    console.log(`Ricerca per: "${query}"`);
    // Qui farai chiamata API
}, 500);

// Simula digitazione
search("j");      // Cancellata
search("ja");     // Cancellata
search("jav");    // Cancellata
search("java");   // Eseguita dopo 500ms

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
Mostra la soluzione

Soluzione Esercizio 2

// Pipeline: esegue funzioni in sequenza
function pipeline(...funzioni) {
    return function(valore) {
        return funzioni.reduce((acc, fn) => fn(acc), valore);
    };
}

// Funzioni di trasformazione
const aggiungi10 = x => x + 10;
const moltiplica3 = x => x * 3;
const sottrai5 = x => x - 5;
const quadrato = x => x * x;

// Crea pipeline
const trasforma = pipeline(
    aggiungi10,
    moltiplica3,
    sottrai5,
    quadrato
);

console.log(trasforma(5));
// 5 → +10 = 15 → *3 = 45 → -5 = 40 → ^2 = 1600

// Pipeline per stringhe
const maiuscola = s => s.toUpperCase();
const inverti = s => s.split("").reverse().join("");
const aggiungiPrefisso = s => `>>> ${s} <<<`;

const trasformaStringa = pipeline(
    maiuscola,
    inverti,
    aggiungiPrefisso
);

console.log(trasformaStringa("ciao"));
// "ciao" → "CIAO" → "OAIC" → ">>> OAIC <<<"

// Versione alternativa con compose (ordine inverso)
function compose(...funzioni) {
    return function(valore) {
        return funzioni.reduceRight((acc, fn) => fn(acc), valore);
    };
}

const trasforma2 = compose(
    quadrato,
    sottrai5,
    moltiplica3,
    aggiungi10
);

console.log(trasforma2(5));  // Stesso risultato: 1600

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
Mostra la soluzione

Soluzione Esercizio 3

// Memoization: cache risultati per performance
function memoize(fn) {
    const cache = {};  // Closure: mantiene cache

    return function(...args) {
        const key = JSON.stringify(args);

        if (key in cache) {
            console.log(`📦 Cache hit per: ${args}`);
            return cache[key];
        }

        console.log(`🔄 Calcolo per: ${args}`);
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

// Fibonacci senza memoization (lento!)
function fibonacciSlow(n) {
    if (n <= 1) return n;
    return fibonacciSlow(n - 1) + fibonacciSlow(n - 2);
}

// Fibonacci con memoization (veloce!)
const fibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
});

// Test performance
console.log("=== SENZA MEMOIZATION ===");
console.time("Slow");
console.log(fibonacciSlow(35));  // Molto lento!
console.timeEnd("Slow");

console.log("\n=== CON MEMOIZATION ===");
console.time("Fast");
console.log(fibonacci(35));  // Velocissimo!
console.timeEnd("Fast");

console.log("\n=== CHIAMATE SUCCESSIVE ===");
console.log(fibonacci(35));  // Dalla cache!
console.log(fibonacci(40));  // Usa cache parzialmente

// Altro esempio: operazione costosa
const operazioneCostosa = memoize((a, b) => {
    console.log(`  Calcolo ${a} ^ ${b}...`);
    return Math.pow(a, b);
});

console.log("\n=== POTENZE ===");
console.log(operazioneCostosa(2, 10));   // Calcola
console.log(operazioneCostosa(2, 10));   // Cache
console.log(operazioneCostosa(3, 5));    // Calcola
console.log(operazioneCostosa(2, 10));   // Cache

// Output:
// === SENZA MEMOIZATION ===
// Slow: 215.432ms
//
// === CON MEMOIZATION ===
// 🔄 Calcolo per: 35
// (molte altre chiamate...)
// Fast: 5.821ms (molto più veloce!)
//
// === CHIAMATE SUCCESSIVE ===
// 📦 Cache hit per: 35
// 🔄 Calcolo per: 40
// (usa cache per 35, calcola solo nuovi)

🎯 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