Definizione di Funzioni in JavaScript

Concetti Chiave
  • Le funzioni in JavaScript possono essere definite in vari modi: dichiarazioni di funzione, espressioni di funzione, arrow function e metodi shorthand.
  • Le dichiarazioni di funzione sono definite con la parola chiave function e possono essere invocate prima della loro definizione grazie al sollevamento (hoisting).
  • Le espressioni di funzione sono definite come parte di un'espressione e non possono essere invocate prima della loro definizione.
  • Le arrow function offrono una sintassi concisa e non hanno il proprio contesto this, ereditandolo dal contesto esterno.
  • I metodi shorthand permettono di definire funzioni come proprietà di un oggetto in modo più conciso.

Introduzione alle Funzioni

A partire da questa lezione inizieremo a studiare le funzioni JavaScript.

Le funzioni sono un componente fondamentale per i programmi JavaScript e una caratteristica comune in quasi tutti i linguaggi di programmazione. Spesso, in altri linguaggi di programmazione, le funzioni sono chiamate spesso procedure o routine.

Una funzione è un blocco di codice JavaScript che viene definito una volta ma può essere eseguito, o invocato, un numero qualsiasi di volte. Le funzioni JavaScript sono parametrizzate: una definizione di funzione può includere un elenco di identificatori, noti come parametri, che funzionano come variabili locali per il corpo della funzione. Le invocazioni di funzioni forniscono valori, o argomenti, per i parametri della funzione.

Le funzioni spesso utilizzano i loro valori di argomento per calcolare un valore di ritorno che diventa il valore dell'espressione di invocazione della funzione. Oltre agli argomenti, ogni invocazione ha un altro valore, il cosiddetto contesto di invocazione, che è il valore della parola chiave this.

Se[] una funzione viene assegnata a una proprietà di un oggetto, è nota come metodo di quell'oggetto. Quando una funzione viene invocata su o attraverso un oggetto, quell'oggetto è il contesto di invocazione o valore this per la funzione. Le funzioni progettate per inizializzare un oggetto appena creato sono chiamate costruttori. Abbiamo introdotto i costruttori nella lezione sugli oggetti, e li tratteremo in dettaglio nelle lezioni sulle classi.

In JavaScript, le funzioni sono esse stesse oggetti a loro volta, e possono essere manipolate dai programmi. JavaScript può assegnare funzioni a variabili e passarle ad altre funzioni, per esempio. Poiché le funzioni sono oggetti, è possibile impostare proprietà su di esse e persino invocare metodi su di esse.

Le definizioni di funzioni JavaScript possono essere annidate all'interno di altre funzioni, e hanno accesso a qualsiasi variabile che sia nello scope dove sono definite. Questo particolare costrutto di programmazione prende il nome di chiusura lessicale o semplicemente chiusura (in inglese closure), e ciò abilita tecniche di programmazione importanti e potenti.

In questa prima lezione vedremo come definire le funzioni JavaScript.

Definire Funzioni

Il modo più diretto per definire una funzione JavaScript è con la parola chiave function, che può essere usata come dichiarazione o come espressione. ES6 definisce un nuovo modo importante per definire funzioni senza la parola chiave function: le arrow functions (chiamate anche funzioni anonime o funzioni freccia) hanno una sintassi particolarmente compatta e sono utili quando si passa una funzione come argomento a un'altra funzione. In questa lezione vedremo i tre modi principali per definire funzioni JavaScript e rimandiamo i dettagli sui parametri alle prossime lezioni.

Abbiamo già visto una sintassi speciale per definire un tipo di metodo di accesso per definire getter e setter. Si possono usare, infatti, le parole chiave get e set nei letterali oggetto per definire metodi speciali getter e setter di proprietà.

Si noti che le funzioni possono anche essere definite con il costruttore Function(), che studieremo nelle prossime lezioni. Inoltre, JavaScript definisce alcuni tipi specializzati di funzioni: le funzioni generatrici e le funzioni asincrone (che usano le parole chiave async e await). Questi tipi di funzioni sono trattati in dettaglio nelle lezioni successive.

Dichiarazioni di Funzione

Le dichiarazioni di funzione consistono nella parola chiave function, seguita da questi tre elementi:

  • Un identificatore che assegna un nome alla funzione. Il nome è una parte richiesta delle dichiarazioni di funzione: viene utilizzato come nome di una variabile, e l'oggetto funzione appena definito viene assegnato alla variabile.
  • Una coppia di parentesi attorno a una lista separata da virgole di zero o più identificatori. Questi identificatori sono i nomi dei parametri per la funzione, e si comportano come variabili locali all'interno del corpo della funzione.
  • Una coppia di parentesi graffe con zero o più istruzioni JavaScript all'interno. Queste istruzioni sono il corpo della funzione: vengono eseguite ogni volta che la funzione viene invocata.

Ecco alcuni esempi di dichiarazioni di funzione:

// Stampa il nome e il valore di ogni proprietà di o. Restituisce undefined.
function stampaProp(o) {
    for(let p in o) {
        console.log(`${p}: ${o[p]}\n`);
    }
}

// Calcola la distanza tra i punti cartesiani (x1,y1) e (x2,y2).
function distanza(x1, y1, x2, y2) {
    let dx = x2 - x1;
    let dy = y2 - y1;
    return Math.sqrt(dx*dx + dy*dy);
}

// Una funzione ricorsiva (una che chiama se stessa) che calcola i fattoriali
// Ricorda che x! è il prodotto di x e tutti gli interi positivi minori di esso.
function fattoriale(x) {
    if (x <= 1) return 1;
    return x * fattoriale(x-1);
}

Una delle cose importanti da capire riguardo alle dichiarazioni di funzione è che il nome della funzione diventa una variabile il cui valore è la funzione stessa. Le istruzioni di dichiarazione di funzione vengono "sollevate" in cima allo script, funzione o blocco che le racchiude, così che le funzioni definite in questo modo possano essere invocate da codice che appare prima della definizione. In altre parole, tutte le funzioni dichiarate in un blocco di codice JavaScript saranno definite in tutto quel blocco, e saranno definite prima che l'interprete JavaScript inizi a eseguire qualsiasi codice in quel blocco.

Le funzioni distanza() e fattoriale() che abbiamo descritto sono progettate per calcolare un valore, e utilizzano return per restituire quel valore al loro chiamante. L'istruzione return causa l'arresto dell'esecuzione della funzione e la restituzione del valore della sua espressione (se presente) al chiamante. Se l'istruzione return non ha un'espressione associata, il valore di ritorno della funzione è undefined.

La funzione stampaProp() è diversa: il suo compito è quello di stampare i nomi e i valori delle proprietà di un oggetto. Non è necessario alcun valore di ritorno, e la funzione non include un'istruzione return. Il valore di un'invocazione della funzione stampaProp() è sempre undefined. Se una funzione non contiene un'istruzione return, semplicemente esegue ogni istruzione nel corpo della funzione fino a raggiungere la fine, e restituisce il valore undefined al chiamante.

Prima di ES6, le dichiarazioni di funzione erano consentite solo al livello superiore all'interno di un file JavaScript o all'interno di un'altra funzione. Mentre alcune implementazioni rilassavano questa regola, non era tecnicamente legale definire funzioni all'interno del corpo di cicli, condizionali o altri blocchi. Nella modalità strict di ES6, tuttavia, le dichiarazioni di funzione sono consentite all'interno dei blocchi. Una funzione definita all'interno di un blocco esiste solo all'interno di quel blocco, tuttavia, e non è visibile all'esterno del blocco.

Funzioni Espressione

Le funzioni espressione assomigliano molto alle dichiarazioni di funzione, ma appaiono all'interno del contesto di un'espressione o istruzione più ampia, e il nome è opzionale. Ecco alcuni esempi di funzioni espressione:

// Questa espressione di funzione definisce una funzione che eleva al quadrato il suo argomento.
// Si noti che viene assegnata a una variabile
const quadrato = function(x) { return x*x; };

// Le funzioni espressione possono includere nomi, il che è utile per la ricorsione.
const f = function fatto(x) { if (x <= 1) return 1; else return x*fatto(x-1); };

// Le funzioni espressione possono anche essere usate come argomenti ad altre funzioni:
[3,2,1].sort(function(a,b) { return a-b; });

// Le funzioni espressione sono talvolta definite e immediatamente invocate:
let dieciAlQuadrato = (function(x) {return x*x;}(10));

Si noti che il nome della funzione è opzionale per le funzioni definite come espressioni, e la maggior parte delle funzioni espressione precedenti che abbiamo mostrato lo omette.

Una dichiarazione di funzione in realtà dichiara una variabile e le assegna un oggetto funzione. Un'espressione di funzione, d'altra parte, non dichiara una variabile: spetta a noi assegnare l'oggetto funzione appena definito a una costante o variabile se abbiamo bisogno di farvi riferimento più volte. È una buona pratica usare const con le funzioni espressione in modo da non sovrascrivere accidentalmente le nostre funzioni assegnando nuovi valori.

Un nome è consentito per le funzioni, come la funzione fattoriale, che hanno bisogno di riferirsi a se stesse. Se un'espressione di funzione include un nome, l'ambito locale della funzione per quella funzione includerà un legame di quel nome all'oggetto funzione. In effetti, il nome della funzione diventa una variabile locale all'interno della funzione. La maggior parte delle funzioni definite come espressioni non ha bisogno di nomi, il che rende la loro definizione più compatta (anche se non così compatta come le funzioni freccia, descritte di seguito).

C'è una differenza importante tra definire una funzione f() con una dichiarazione di funzione e assegnare una funzione alla variabile f dopo averla creata come espressione. Quando usiamo la forma di dichiarazione, gli oggetti funzione vengono creati prima che il codice che li contiene inizi a essere eseguito, e le definizioni vengono issate così che possiamo chiamare queste funzioni da codice che appare sopra l'istruzione di definizione. Questo non è vero per le funzioni definite come espressioni, tuttavia: queste funzioni non esistono fino a quando l'espressione che le definisce non viene effettivamente valutata. Inoltre, per invocare una funzione, dobbiamo essere in grado di farvi riferimento, e non possiamo riferirci a una funzione definita come espressione fino a quando non viene assegnata a una variabile, quindi le funzioni definite con espressioni non possono essere invocate prima di essere definite.

Arrow Functions o Funzioni Freccia

In ES6, possiamo definire funzioni utilizzando una sintassi particolarmente compatta conosciuta come funzioni freccia (chiamate anche funzioni anonime). Questa sintassi ricorda la notazione matematica e utilizza una "freccia" => per separare i parametri della funzione dal corpo della funzione. La parola chiave function non viene utilizzata e, poiché le funzioni freccia sono espressioni invece che istruzioni, non c'è bisogno nemmeno di un nome per la funzione. La forma generale di una funzione freccia è una lista di parametri separati da virgole tra parentesi, seguita dalla freccia =>, seguita dal corpo della funzione tra parentesi graffe:

const somma = (x, y) => { return x + y; };

Ma le funzioni freccia supportano una sintassi ancora più compatta. Se il corpo della funzione è una singola istruzione return, possiamo omettere la parola chiave return, il punto e virgola che la accompagna, e le parentesi graffe, e scrivere il corpo della funzione come l'espressione il cui valore deve essere restituito:

const somma = (x, y) => x + y;

Inoltre, se una funzione freccia ha esattamente un parametro, possiamo omettere le parentesi attorno alla lista dei parametri:

const polinomio = x => x*x + 2*x + 3;

Si noti, tuttavia, che una funzione freccia senza argomenti deve essere scritta con una coppia vuota di parentesi:

const funzioneConstante = () => 42;

Un'altra importante osservazione è che, quando scriviamo una funzione freccia, non dobbiamo mettere una nuova riga tra i parametri della funzione e la freccia =>. Altrimenti, potremmo finire con una riga come const polinomio = x, che è un'istruzione di assegnazione sintatticamente valida da sola.

Inoltre, se il corpo della nostra funzione freccia è una singola istruzione return ma l'espressione da restituire è un oggetto letterale, allora dobbiamo mettere l'oggetto letterale tra parentesi per evitare ambiguità sintattiche tra le parentesi graffe di un corpo di funzione e le parentesi graffe di un oggetto letterale:

const f = x => { return { valore: x }; };  // Corretto: f() restituisce un oggetto
const g = x => ({ valore: x });            // Corretto: g() restituisce un oggetto
const h = x => { valore: x };              // Sbagliato: h() non restituisce nulla
const i = x => { v: x, w: x };             // Sbagliato: Errore di Sintassi

Nella terza riga di questo codice, la funzione h() è veramente ambigua: il codice che intendevamo come oggetto letterale può essere interpretato come un'istruzione etichettata, quindi viene creata una funzione che restituisce undefined. Nella quarta riga, tuttavia, l'oggetto letterale più complicato non è un'istruzione valida, e questo codice illegale causa un errore di sintassi.

La sintassi concisa delle funzioni freccia le rende ideali quando dobbiamo passare una funzione a un'altra funzione, cosa comune da fare con metodi di array come map(), filter(), e reduce(), per esempio:

// Crea una copia di un array con elementi null rimossi.
let filtrato = [1,null,2,3].filter(x => x !== null); // filtrato == [1,2,3]
// Eleva al quadrato alcuni numeri:
let quadrati = [1,2,3,4].map(x => x*x);              // quadrati == [1,4,9,16]

Le funzioni freccia differiscono fondamentale dalle funzioni definite in altri modi: ereditano il valore della parola chiave this dall'ambiente in cui sono definite piuttosto che definire il proprio contesto di invocazione come fanno le funzioni definite in altri modi. Questa è una caratteristica importante e molto utile delle funzioni freccia, e torneremo su di essa più avanti nelle prossime lezioni. Le funzioni freccia differiscono anche dalle altre funzioni in quanto non hanno una proprietà prototype, il che significa che non possono essere utilizzate come funzioni costruttore per nuove classi.

Metodi Shorthand

In ES6, la sintassi degli oggetti letterali (e anche la sintassi di definizione delle classi che vedremo nelle prossime lezioni) è stata estesa per consentire una scorciatoia dove la parola chiave function e i due punti sono omessi, risultando in codice come questo:

let quadrato = {
    area() { return this.lato * this.lato; },
    lato: 10
};

quadrato.area() // => 100

Entrambe le forme del codice, con function e senza, sono equivalenti: entrambe aggiungono una proprietà chiamata area all'oggetto letterale, ed entrambe impostano il valore di quella proprietà alla funzione specificata. Questa sintassi prende il nome di metodo shorthand o metodo accorciato.

La sintassi shorthand rende più chiaro che area() è un metodo e non una proprietà di dati come lato.

Quando si scrive un metodo usando questa sintassi shorthand, il nome della proprietà può assumere qualsiasi delle forme che sono legali in un oggetto letterale: oltre a un identificatore JavaScript regolare come il nome area sopra, si possono anche usare letterali stringa e nomi di proprietà calcolati, che possono includere nomi di proprietà Symbol:

const NOME_METODO = "m";
const simbolo = Symbol();
let metodiStrani = {
    "metodo Con Spazi"(x) { return x + 1; },
    [NOME_METODO](x) { return x + 2; },
    [simbolo](x) { return x + 3; }
};
metodiStrani["metodo Con Spazi"](1)  // => 2
metodiStrani[NOME_METODO](1)           // => 3
metodiStrani[simbolo](1)                // => 4

Usare un Symbol come nome di metodo non è così strano come sembra. Per rendere un oggetto iterabile (così può essere usato con un ciclo for/of), bisogna definire un metodo con il nome simbolico Symbol.iterator e vedremo come fare nelle prossime lezioni.

Funzioni Annidate

In JavaScript, le funzioni possono essere annidate all'interno di altre funzioni. Ad esempio:

function ipotenusa(a, b) {
    function quadrato(x) { return x*x; }
    return Math.sqrt(quadrato(a) + quadrato(b));
}

La cosa interessante delle funzioni annidate sono le loro regole di scoping delle variabili: possono accedere ai parametri e alle variabili della funzione (o funzioni) all'interno delle quali sono annidate. Nel codice mostrato qui, ad esempio, la funzione interna quadrato() può leggere e scrivere i parametri a e b definiti dalla funzione esterna ipotenusa(). Queste regole di scope per le funzioni annidate sono molto importanti, e ci torneremo su nelle prossime lezioni.