Parametri e Argomenti di una Funzione in JavaScript

Concetti Chiave
  • I parametri di una funzione possono essere opzionali, e i valori di default possono essere specificati per i parametri.
  • I parametri rest permettono di raccogliere un numero variabile di argomenti in un array.
  • L'oggetto arguments fornisce accesso agli argomenti passati a una funzione, ma non è un array e non supporta i metodi degli array.
  • La destrutturazione degli oggetti e degli array può essere utilizzata per semplificare la definizione e l'invocazione delle funzioni.
  • La destrutturazione può essere utilizzata per passare oggetti e array come argomenti, permettendo di specificare solo le proprietà o gli elementi necessari.

Argomenti e Parametri delle Funzioni

Le definizioni delle funzioni JavaScript non specificano un tipo previsto per i parametri della funzione, e le invocazioni delle funzioni non eseguono alcun controllo di tipo sui valori degli argomenti che vengono passati. Infatti, le invocazioni delle funzioni JavaScript non controllano nemmeno il numero di argomenti che vengono passati.

In questa lezione studiamo cosa accade quando una funzione viene invocata con meno argomenti rispetto ai parametri dichiarati o con più argomenti rispetto ai parametri dichiarati. Vedremo anche come testare esplicitamente il tipo degli argomenti delle funzioni se è necessario assicurarsi che una funzione non venga invocata con argomenti inappropriati.

Parametri Opzionali e Valori di Default

Quando una funzione viene invocata con meno argomenti rispetto ai parametri dichiarati, i parametri aggiuntivi sono impostati al loro valore di default, che normalmente è undefined.

È spesso utile scrivere funzioni in modo che alcuni argomenti siano opzionali. Segue un esempio:

// Aggiungi i nomi delle proprietà enumerabili dell'oggetto o all'
// array a, e restituisci a. Se a è omesso, crea e restituisci un nuovo array.
function ottieniNomiProprietà(o, a) {
    // Se undefined, usa un nuovo array
    if (a === undefined) {
        a = [];
    }

    for (let proprietà in o) {
        a.push(proprietà);
    }

    return a;
}

// ottieniNomiProprietà() può essere invocata con uno o due argomenti:
// Due oggetti per il test
let o = {x: 1}, p = {y: 2, z: 3};

// a == ["x"]; ottieni le proprietà di o in un nuovo array
let a = ottieniNomiProprietà(o);

// a == ["x","y","z"]; aggiungi le proprietà di p ad esso
ottieniNomiProprietà(p, a);

Invece di usare un'istruzione if nella prima riga di questa funzione, si può usare l'operatore || in questo modo idiomatico:

a = a || [];

Ricordiamo che l'operatore || restituisce il suo primo argomento se quell'argomento è true e altrimenti restituisce il suo secondo argomento. In questo caso, se qualsiasi oggetto viene passato come secondo argomento, la funzione userà quell'oggetto. Ma se il secondo argomento è omesso (o null o un altro valore false viene passato), verrà usato invece un array vuoto appena creato.

Si noti che quando si progettano funzioni con argomenti opzionali, bisogna assicurarsi di mettere quelli opzionali alla fine della lista degli argomenti in modo che possano essere omessi. Il programmatore che chiama la funzione non può omettere il primo argomento e passare il secondo: deve passare esplicitamente undefined come primo argomento.

In ES6 e successivi, è possibile definire un valore di default per ciascuno dei parametri di funzione direttamente nella lista dei parametri della funzione. È sufficiente seguire il nome del parametro con un segno di uguale e il valore di default da usare quando nessun argomento viene fornito per quel parametro:

// Aggiungi i nomi delle proprietà enumerabili dell'oggetto o all'
// array a, e restituisci a. Se a è omesso, crea e restituisci un nuovo array.
function ottieniNomiProprietà(o, a = []) {
    for (let proprietà in o) {
        a.push(proprietà);
    }

    return a;
}

Le espressioni di default dei parametri sono valutate quando la funzione viene chiamata, non quando viene definita, quindi ogni volta che questa funzione ottieniNomiProprietà() viene invocata con un argomento, un nuovo array vuoto viene creato e passato. È probabilmente più facile ragionare sulle funzioni se i default dei parametri sono costanti (o espressioni letterali come [] e {}). Ma questo non è richiesto: si possono usare variabili, o invocazioni di funzioni, ad esempio, per calcolare il valore di default di un parametro. Un caso interessante è che, per funzioni con parametri multipli, si può usare il valore di un parametro precedente per definire il valore di default dei parametri che lo seguono:

// Questa funzione restituisce un oggetto che rappresenta le dimensioni di un rettangolo.
// Se viene fornita solo la larghezza, rendilo alto il doppio di quanto è largo.
const rettangolo = (larghezza, altezza = larghezza * 2) => ({
    larghezza, 
    altezza
});

// => { larghezza: 1, altezza: 2 }
rettangolo(1);

Questo codice dimostra che i default dei parametri funzionano con le arrow function. Lo stesso vale per le funzioni shorthand dei metodi e tutte le altre forme di definizioni di funzioni.

Parametri Rest e Liste di Argomenti a Lunghezza Variabile

I valori predefiniti dei parametri ci permettono di scrivere funzioni che possono essere invocate con meno argomenti rispetto ai parametri. I parametri rest abilitano il caso opposto: ci permettono di scrivere funzioni che possono essere invocate con arbitrariamente più argomenti rispetto ai parametri. Ecco un esempio di funzione che si aspetta uno o più argomenti numerici e restituisce il più grande:

function max(primo = -Infinity, ...resto) {
    // Inizia assumendo che il primo arg sia il più grande
    let valoreMassimo = primo;

    // Poi itera attraverso il resto degli argomenti, cercando uno più grande
    for (let n of resto) {
        if (n > valoreMassimo) {
            valoreMassimo = n;
        }
    }

    // Restituisce il più grande
    return valoreMassimo;
}

// => 1000
max(1, 10, 100, 2, 3, 1000, 4, 5, 6);

Un parametro rest è preceduto da tre punti, e deve essere l'ultimo parametro in una dichiarazione di funzione. Quando invochiamo una funzione con un parametro rest, gli argomenti che passiamo sono prima assegnati ai parametri non-rest, e poi tutti gli argomenti rimanenti (cioè, il "resto" degli argomenti) sono memorizzati in un array che diventa il valore del parametro rest. Quest'ultimo punto è importante: all'interno del corpo di una funzione, il valore di un parametro rest sarà sempre un array. L'array può essere vuoto, ma un parametro rest non sarà mai undefined. (Ne consegue che non è mai utile, e non è legale, definire un valore predefinito per un parametro rest.)

Le funzioni come l'esempio precedente che possono accettare qualsiasi numero di argomenti sono chiamate funzioni variadiche, funzioni ad arità variabile, o funzioni vararg. Questa guida usa il termine più colloquiale, varargs, che risale ai primi giorni del linguaggio di programmazione C.

Non bisogna confondere i ... che definiscono un parametro rest in una definizione di funzione con l'operatore spread ... che può essere utilizzato nelle invocazioni di funzioni.

L'Oggetto Arguments

I parametri rest sono stati introdotti in JavaScript in ES6. Prima di quella versione del linguaggio, le funzioni varargs venivano scritte usando l'oggetto Arguments: all'interno del corpo di qualsiasi funzione, l'identificatore arguments fa riferimento all'oggetto Arguments per quella invocazione. L'oggetto Arguments è un oggetto simile a un array che consente ai valori degli argomenti passati alla funzione di essere recuperati per numero, piuttosto che per nome. Ecco la funzione massimo() di prima, riscritta per usare l'oggetto Arguments invece di un parametro rest:

function massimo(x) {
    let valoreMassimo = -Infinity;

    // Cicla attraverso gli argomenti, cercando e ricordando il più grande.
    for (let i = 0; i < arguments.length; i++) {
        if (arguments[i] > valoreMassimo) {
            valoreMassimo = arguments[i];
        }
    }

    // Restituisce il più grande
    return valoreMassimo;
}

// => 1000
massimo(1, 10, 100, 2, 3, 1000, 4, 5, 6);

L'oggetto Arguments risale ai primi giorni di JavaScript e porta con sé alcuni strani bagagli storici che lo rendono inefficiente e difficile da ottimizzare, specialmente al di fuori della modalità strict. Si può ancora incontrare codice che usa l'oggetto Arguments, ma si dovrebbe evitare di usarlo in qualsiasi nuovo codice che si scrive. Quando si rifattorizza vecchio codice, se si incontra una funzione che usa arguments, si può spesso sostituirla con un parametro rest ...args. Parte del patrimonio sfortunato dell'oggetto Arguments è che, in modalità strict, arguments è trattato come una parola riservata, e non si può dichiarare un parametro di funzione o una variabile locale con quel nome.

L'Operatore Spread per le Chiamate di Funzione

L' operatore spread ... viene utilizzato per decomprimere, o "espandere", gli elementi di un array (o qualsiasi altro oggetto iterabile, come le stringhe) in un contesto dove vengono attesi valori individuali. Abbiamo visto l'operatore spread utilizzato con i letterali di array nella lezione apposita.

L'operatore può essere utilizzato, nello stesso modo, nelle invocazioni di funzioni:

let numeri = [5, 2, 10, -1, 9, 100, 1];

// => -1
Math.min(...numeri);

Si noti che ... non è un vero operatore nel senso che non può essere valutato per produrre un valore. Invece, è una sintassi JavaScript speciale che può essere utilizzata nei letterali di array e nelle invocazioni di funzioni.

Quando si utilizza la stessa sintassi ... in una definizione di funzione piuttosto che in un'invocazione di funzione, ha l'effetto opposto all'operatore spread.

Come abbiamo visto prima, utilizzare ... in una definizione di funzione raccoglie multipli argomenti di funzione in un array. I parametri rest e l'operatore spread sono spesso utili insieme, come nella seguente funzione, che prende un argomento di funzione e restituisce una versione strumentata della funzione per il testing:

// Questa funzione prende una funzione e restituisce una versione avvolta
function cronometrata(f) {
    return function(...args) {
        // Raccoglie args in un array di parametri rest
        console.log(`Ingresso nella funzione ${f.name}`);
        let tempoInizio = Date.now();

        try {
            // Passa tutti i nostri argomenti alla funzione avvolta
            // Espande nuovamente gli args
            return f(...args);
        }
        finally {
            // Prima di restituire il valore di ritorno avvolto, stampa il tempo trascorso.
            console.log(`Uscita da ${f.name} dopo ${Date.now() - tempoInizio}ms`);
        }
    };
}

// Calcola la somma dei numeri tra 1 e n con forza bruta
function benchmark(n) {
    let somma = 0;

    for (let i = 1; i <= n; i++) {
        somma += i;
    }

    return somma;
}

// Ora invoca la versione cronometrata di quella funzione di test
// => 500000500000; questa è la somma dei numeri
cronometrata(benchmark)(1000000);

Destrutturazione degli Argomenti di Funzione in Parametri

Quando si invoca una funzione con una lista di valori argomento, quei valori finiscono per essere assegnati ai parametri dichiarati nella definizione della funzione. Questa fase iniziale dell'invocazione di funzione è molto simile all'assegnazione di variabili. Quindi non dovrebbe sorprendere che si possano usare le tecniche dell'assegnazione destrutturante con le funzioni.

Se si definisce una funzione che ha nomi di parametri all'interno di parentesi quadre, si sta dicendo alla funzione di aspettarsi che un valore array venga passato per ogni coppia di parentesi quadre. Come parte del processo di invocazione, gli argomenti array verranno spacchettati nei parametri nominati individualmente. Come esempio, si supponga di rappresentare i vettori 2D come array di due numeri, dove il primo elemento è la coordinata X e il secondo elemento è la coordinata Y. Con questa semplice struttura dati, si potrebbe scrivere la seguente funzione per sommare due vettori:

function sommaVettori(v1, v2) {
    return [v1[0] + v2[0], v1[1] + v2[1]];
}

// => [4,6]
sommaVettori([1,2], [3,4]);

Il codice sarebbe più facile da capire se si destrutturassero i due argomenti vettore in parametri nominati più chiaramente:

// Spacchetta 2 argomenti in 4 parametri
function sommaVettori([x1, y1], [x2, y2]) {
    return [x1 + x2, y1 + y2];
}

// => [4,6]
sommaVettori([1,2], [3,4]);

Similmente, se si sta definendo una funzione che si aspetta un argomento oggetto, si possono destrutturare i parametri di quell'oggetto. Si usa di nuovo un esempio di vettore, eccetto che questa volta, si suppone di rappresentare i vettori come oggetti con parametri x e y:

// Moltiplica il vettore {x,y} per un valore scalare
function moltiplicaVettore({x, y}, scalare) {
    return { x: x * scalare, y: y * scalare };
}

// => {x: 2, y: 4}
moltiplicaVettore({x: 1, y: 2}, 2);

Questo esempio di destrutturazione di un singolo argomento oggetto in due parametri è abbastanza chiaro perché i nomi dei parametri che si usano corrispondono ai nomi delle proprietà dell'oggetto in arrivo. La sintassi è più verbosa e più confusa quando si deve destrutturare proprietà con un nome in parametri con nomi diversi. Ecco l'esempio di addizione di vettori, implementato per vettori basati su oggetti:

function sommaVettori(
    // Spacchetta il primo oggetto nei parametri x1 e y1
    {x: x1, y: y1},
    // Spacchetta il secondo oggetto nei parametri x2 e y2
    {x: x2, y: y2}
) {
    return { x: x1 + x2, y: y1 + y2 };
}

// => {x: 4, y: 6}
sommaVettori({x: 1, y: 2}, {x: 3, y: 4});

La cosa difficile della sintassi di destrutturazione come {x:x1, y:y1} è ricordare quali sono i nomi delle proprietà e quali sono i nomi dei parametri. La regola da tenere a mente per l'assegnazione destrutturante e le chiamate di funzione destrutturanti è che le variabili o i parametri che vengono dichiarati vanno nei punti dove ci si aspetterebbe che vadano i valori in un oggetto letterale. Quindi i nomi delle proprietà sono sempre sul lato sinistro dei due punti, e i nomi dei parametri (o delle variabili) sono sulla destra.

Si possono definire valori predefiniti dei parametri con parametri destrutturati. Ecco la moltiplicazione di vettori che funziona con vettori 2D o 3D:

// Moltiplica il vettore {x,y} o {x,y,z} per un valore scalare
function moltiplicaVettore({x, y, z = 0}, scalare) {
    return { x: x * scalare, y: y * scalare, z: z * scalare };
}

// => {x: 2, y: 4, z: 0}
moltiplicaVettore({x: 1, y: 2}, 2);

Alcuni linguaggi (come Python) permettono al chiamante di una funzione di invocare una funzione con argomenti specificati nella forma nome=valore, che è conveniente quando ci sono molti argomenti opzionali o quando la lista dei parametri è lunga abbastanza da essere difficile ricordare l'ordine corretto. JavaScript non permette questo direttamente, ma si può approssimarlo destrutturando un argomento oggetto nei parametri della funzione. Si consideri una funzione che copia un numero specificato di elementi da un array in un altro array con offset di partenza opzionalmente specificati per ogni array. Dato che ci sono cinque possibili parametri, alcuni dei quali hanno valori predefiniti, e sarebbe difficile per un chiamante ricordare in quale ordine passare gli argomenti, si possono definire e invocare la funzione copiaArray() così:

function copiaArray({from, to = from, n = from.length, fromIndex = 0, toIndex = 0}) {
    let valoriDaCopiare = from.slice(fromIndex, fromIndex + n);
    to.splice(toIndex, 0, ...valoriDaCopiare);
    return to;
}

let a = [1, 2, 3, 4, 5], b = [9, 8, 7, 6, 5];

// => [9,8,7,6,1,2,3,5]
copiaArray({from: a, n: 3, to: b, toIndex: 4});

Quando destrutturiamo un array, possiamo definire un parametro rest per valori extra all'interno dell'array che viene spacchettato. Quel parametro rest all'interno delle parentesi quadre è completamente diverso dal vero parametro rest per la funzione:

// Questa funzione si aspetta un argomento array. I primi due elementi di
// quell'array vengono spacchettati nei parametri x e y. Qualsiasi elemento
// rimanente viene memorizzato nell'array coords. E qualsiasi argomento dopo
// il primo array viene impacchettato nell'array rest.
function f([x, y, ...coords], ...rest) {
    // Nota: operatore spread qui
    return [x + y, ...rest, ...coords];
}

// => [3, 5, 6, 3, 4]
f([1, 2, 3, 4], 5, 6);

In ES2018, si può anche usare un parametro rest quando si destruttura un oggetto. Il valore di quel parametro rest sarà un oggetto che ha qualsiasi proprietà che non è stata destrutturata. I parametri rest degli oggetti sono spesso utili con l'operatore spread degli oggetti, che è anche una nuova caratteristica di ES2018:

// Moltiplica il vettore {x,y} o {x,y,z} per un valore scalare, mantiene altre prop
function moltiplicaVettore({x, y, z = 0, ...props}, scalare) {
    return { x: x * scalare, y: y * scalare, z: z * scalare, ...props };
}

// => {x: 2, y: 4, z: 0, w: -1}
moltiplicaVettore({x: 1, y: 2, w: -1}, 2);

Infine, teniamo a mente che, oltre a destrutturare oggetti e array argomento, si possono anche destrutturare array di oggetti, oggetti che hanno proprietà array, e oggetti che hanno proprietà oggetto, essenzialmente a qualsiasi profondità. Si consideri codice grafico che rappresenta i cerchi come oggetti con proprietà x, y, radius, e color, dove la proprietà color è un array di componenti di colore rosso, verde e blu. Si potrebbe definire una funzione che si aspetta che un singolo oggetto cerchio venga passato ad essa ma destruttura quell'oggetto cerchio in sei parametri separati:

function disegnaCerchio({x, y, radius, color: [r, g, b]}) {
    // Non ancora implementato
}

Se la destrutturazione degli argomenti di funzione è più complicata di così, si trova che il codice diventa più difficile da leggere, piuttosto che più semplice. A volte, è più chiaro essere espliciti riguardo all'accesso alle proprietà degli oggetti e all'indicizzazione degli array.

Tipi di Argomenti

In JavaScript, i tipi di argomenti non sono specificati nelle definizioni delle funzioni. Quindi, quando si scrive una funzione, non si può dire che tipo di argomento ci si aspetta. Questo è diverso da molti altri linguaggi di programmazione, dove le funzioni hanno tipi di argomento specificati e il compilatore o l'interprete controlla che gli argomenti passati corrispondano ai tipi attesi.

Inoltre, non viene eseguito alcun controllo dei tipi sui valori che passiamo a una funzione. Possiamo aiutare a rendere il nostro codice auto-documentante scegliendo nomi descrittivi per gli argomenti delle funzioni e documentandoli accuratamente nei commenti per ogni funzione.

Come descritto nelle lezioni precedenti, JavaScript esegue conversioni di tipo in modo molto flessibile quando necessario. Quindi se si scrive una funzione che si aspetta un argomento stringa e poi si chiama quella funzione con un valore di qualche altro tipo, il valore che si è passato sarà semplicemente convertito in una stringa quando la funzione cerca di usarlo come una stringa. Tutti i tipi primitivi possono essere convertiti in stringhe, e tutti gli oggetti hanno metodi toString(), quindi un errore non si verifica mai in questo caso.

Questo non è sempre vero, tuttavia. Consideriamo nuovamente il metodo copiaArray() mostrato in precedenza. Si aspetta uno o due argomenti array e fallirà se questi argomenti sono del tipo sbagliato. A meno che non si stia scrivendo una funzione privata che sarà chiamata solo da parti vicine del codice, potrebbe valere la pena aggiungere codice per controllare i tipi degli argomenti come questo. È meglio per una funzione fallire immediatamente e prevedibilmente quando vengono passati valori errati piuttosto che iniziare l'esecuzione e fallire dopo con un messaggio di errore che probabilmente sarà poco chiaro. Ecco un esempio di funzione che esegue il controllo dei tipi:

// Restituisce la somma degli elementi di un oggetto iterabile a.
// Gli elementi di a devono essere tutti numeri.
function somma(a) {
    let totale = 0;

    // Lancia TypeError se a non è iterabile
    for (let elemento of a) {
        if (typeof elemento !== "number") {
            throw new TypeError("somma(): gli elementi devono essere numeri");
        }
        totale += elemento;
    }

    return totale;
}

// => 6
somma([1, 2, 3]);

// !TypeError: 1 is not iterable
somma(1, 2, 3);

// !TypeError: element 2 is not a number
somma([1, 2, "3"]);