29º marzo 2021

Funzioni

Molto spesso abbiamo la necessità di eseguire azioni simili in diverse parti dello script.

Ad esempio, vogliamo mostrare un messaggio quando un utente effettua il login/logout o per qualsiasi altro motivo.

Le funzioni sono le “fondamenta” di un programma. Consentono a un codice di essere utilizzato più volte, evitando ripetizioni.

Abbiamo già visto esempi di funzioni integrate nel linguaggio, come alert(message), prompt(message, default) e confirm(question). Ma possiamo benissimo anche creare delle nostre funzioni personali.

Dichiarazione di Funzione

Per creare una funzione dobbiamo utilizzare una dichiarazione di funzione.

Che appare cosi:

function showMessage() {
  alert( 'Hello everyone!' );
}

La parola chiave function va posta all’inizio; viene seguita dal nome della funzione, poi c’è una lista di parametri, racchiusi tra parentesi (in questo esempio la lista è vuota) e infine il codice della funzione, chiamato anche “corpo della funzione”, racchiuso tra parentesi graffe.

function name(parameters) {
  ...body...
}

La nostra nuova funzione può essere chiamata tramite il suo nome: showMessage().

Ad esempio:

function showMessage() {
  alert( 'Hello everyone!' );
}

showMessage();
showMessage();

La chiamata showMessage() esegue il codice dentro la funzione. Qui vedremo il messaggio due volte.

Questo esempio mostra chiaramente uno dei principali scopi delle funzioni: evitare le ripetizioni di un codice.

Se avremo la necessità di cambiare il messaggio o il modo in cui viene mostrato, sarà sufficiente modificare il codice in un solo punto: la funzione che lo implementa.

Variabili locali

Una variabile dichiarata all’interno di una funzione è visibile solamente all’interno di quella funzione.

Ad esempio:

function showMessage() {
  let message = "Hello, I'm JavaScript!"; // variabile locale

  alert( message );
}

showMessage(); // Hello, I'm JavaScript!

alert( message ); // <-- Errore! La variabile è locale alla funzione

Variabili esterne

Una funzione può accedere ad una variabile esterna, ad esempio:

let userName = 'John';

function showMessage() {
  let message = 'Hello, ' + userName;
  alert(message);
}

showMessage(); // Hello, John

La funzione ha pieno accesso alla variabile esterna. Può anche modificarla.

Un esempio:

let userName = 'John';

function showMessage() {
  userName = "Bob"; // (1) cambiata la variabile esterna

  let message = 'Hello, ' + userName;
  alert(message);
}

alert( userName ); // John prima della chiamata di funzione

showMessage();

alert( userName ); // Bob, il valore è stato modificato dalla funzione

La variabile esterna viene utilizzata solo se non ce n’è una locale.

Se una variabile con lo stesso nome viene dichiarata all’interno di una funzione, questa oscurerà quella esterna. Ad esempio, nel codice sotto la funzione usa la variabile locale userName. Quella esterna viene ignorata:

let userName = 'John';

function showMessage() {
  let userName = "Bob"; // dichiara una variabile locale

  let message = 'Hello, ' + userName; // Bob
  alert(message);
}

// la funzione creerà è utilizzerà un suo 'personale' userName
showMessage();

alert( userName ); // John, intoccato, la funzione non può accedere alla variabile esterna
Variabili globali

Le variabili dichiarate all’esterno di qualsiasi funzione, come userName nel codice sopra, vengono chiamate globali.

Le variabili globali sono visibili a qualsiasi funzione (se non sono oscurate da quelle locali).

Solitamente, una funzione dichiara internamente tutte le variabili necessarie per svolgere il suo compito. Le variabili locali vengono utilizzate per memorizzare dati relativi alla funzione stessa, quando è importante che queste non siano accessibili in qualsiasi punto del codice. I codici moderni cercano di evitare le variabili globali, sebbene qualche volta possano essere utili per dati

Parametri

Possiamo passare dei dati arbitrari ad una funzione usando i parametri (chiamati anche argomenti della funzione).

Nell’esempio sotto, la funzione ha due parametri: from e text.

function showMessage(from, text) { // argomenti: from, text
  alert(from + ': ' + text);
}

showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)

Quando la funzione viene chiamata nelle righe (*) e (**), il valore passato viene copiato nelle variabili locali from e text, che verranno utilizzate nella chiamata ad alert.

Guardiamo un altro esempio: abbiamo una variabile from e la passiamo a una funzione. Da notare: la funzione cambia from, ma il cambiamento non è visibile all’esterno perchè la funzione usa sempre una copia del valore passato:

function showMessage(from, text) {

  from = '*' + from + '*'; // rende "from" più carino

  alert( from + ': ' + text );
}

let from = "Ann";

showMessage(from, "Hello"); // *Ann*: Hello

// il valore di "from" è lo stesso, la funzione ne ha modificato una copia locale
alert( from ); // Ann

Valori di default

Se non viene fornito alcun parametro, questa assume il valore undefined.

Ad esempio, la funzione menzionata sopra showMessage(from, text) può essere chiamata con un solo argomento:

showMessage("Ann");

Questo non è un errore. Una chiamata simile mostrerà "Ann: undefined". Non c’è nessun valore text, quindi si assume che text === undefined.

Se volessimo utilizzare un text di “default”, dovremmo specificarlo dopo =:

function showMessage(from, text = "no text given") {
  alert( from + ": " + text );
}

showMessage("Ann"); // Ann: nessun text fornito

Adesso, se il parametro text non viene passato, assumerà il valore "no text given"

In questo caso "no text given" è una stringa, ma potrebbe essere un’espressione più complessa, che viene valutata e assegnata solamente se il parametro non viene fornito. Quindi, è possibile anche:

function showMessage(from, text = anotherFunction()) {
  // anotherFunction() viene eseguita solamente se non viene fornito text
  // il risultato diventa il valore di text
}
Valutazione dei parametri di default

In JavaScript, un parametro di default viene valutato ogni volta che viene chiamata una funzione senza i rispettivi parametri. Nell’esempio sopra, anotherFunctions() viene richiamata ogni volta che someMessage() viene chiamata senza il parametro text.

#Parametri di default alternativi In alcuni casi vorremmo assegnare un valore di default a un parametro non nella dichiarazione della funzione, ma dopo, durante la sua esecuzione. (inoltre, le vecchie edizioni di JavaScript non supportavano i parametri di default; quel che segue è un metodo per ovviare ad entrambe le necessità, e che potreste trovare di frequente nei vecchi script).

Per controllare se un parametro è stato omesso possiamo compararlo con undefined:

function showMessage(text) {
  if (text === undefined) {
    text = 'empty message';
  }

  alert(text);
}

showMessage(); // empty message

…Oppure utilizare l’operatore ||:

function showMessage(from, text) {
  // se text è falso allora text assume il valore di default
  text = text || 'no text given';
  ...
}

I moderni motori JavaScript supportano il nullish coalescing operator ??, più efficiente quando valori falsi come 0vengono considerati regolari:

//se non c'è un parametro "count", mostra "unknown"
function showCount(count) {
  alert(count ?? "unknown");
}

showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown

Ritornare un valore

Una funzione può anche ritornare, come risultato, un valore al codice che ha effettuato la sua chiamata.

Un semplicissimo esempio è una funzione che somma due valori:

function sum(a, b) {
  return a + b;
}

let result = sum(1, 2);
alert( result ); // 3

La direttiva return può trovarsi in qualsiasi punto della funzione. Quando l’esecuzione incontra questa direttiva, si ferma, e il valore viene ritornato al codice che ha chiamato la funzione (nel codice sopra viene assegnato a result).

Ci possono essere più occorrenze di return in una singola funzione. Ad esempio:

function checkAge(age) {
  if (age >= 18) {
    return true;
  } else {
    return confirm('Do you have permission from your parents?');
  }
}

let age = prompt('How old are you?', 18);

if ( checkAge(age) ) {
  alert( 'Access granted' );
} else {
  alert( 'Access denied' );
}

E’ anche possibile utilizzare return senza alcun valore. Questo causerà l’uscita immediata dalla funzione.

Ad esempio:

function showMovie(age) {
  if ( !checkAge(age) ) {
    return;
  }

  alert( "Showing you the movie" ); // (*)
  // ...
}

Nel codice sopra, se checkAge(age) ritorna false, allora showMovie non procederà fino ad alert.

Una funzione con un return vuoto ritorna undefined

Se una funzione non ritorna alcun valore, è come se ritornasse undefined:

function doNothing() { /* vuoto */ }

alert( doNothing() === undefined ); // true

Un return vuoto è dunque la stessa cosa di return undefined:

function doNothing() {
  return;
}

alert( doNothing() === undefined ); // true
Non aggiungere mai una nuova riga trareturn e il valore

Per espressioni lunghe dopo la direttiva return, si potrebbe essere tentati dal metterle in una nuova riga, come:

return
 (some + long + expression + or + whatever * f(a) + f(b))

Questo non funziona, perchè JavaScript interpreta un punto e virgola dopo return. E’ come se dopo return ci fosse scritto:

return;
 (some + long + expression + or + whatever * f(a) + f(b))

Quindi, diventerebbe a tutti gli effetti un return vuoto. Dobbiamo quindi posizionare il valore da ritornare nella stessa riga.

Denominare una funzione

Le funzioni sono azioni. Solitamente, per il loro nome vengono utilizzati verbi. Dovrebbero essere brevi, il più accurati possibili e descrittivi di ciò che la funzione fa. Un estraneo che legge il codice deve essere in grado di capire ciò che fa la funzione.

Una pratica molto diffusa è quella di iniziare il nome con un verbo, come prefisso, che descriva vagamente l’azione. Deve esserci un accordo con il team riguardo il significato dei prefissi.

Ad esempio, le funzioni che iniziano con "show" solitamente mostrano qualcosa.

Funzioni che iniziano per…

  • "get…" – ritornano un valore,
  • "calc…" – calcolano qualcosa,
  • "create…" – creano qualcosa,
  • "check…" – controllano qualcosa e ritornano un booleano, etc.

Esempi di nomi:

showMessage(..)     // mostra un messaggio
getAge(..)          // ritonra l'età (prendendola da qualche parte)
calcSum(..)         // calcola la somma e ritorna il risultato
createForm(..)      // crea un form (e solitamente lo ritorna)
checkPermission(..) // controlla i permessi, ritorna true/false

Tramite il prefisso, una semplice occhiata al nome della funzione dovrebbe far capire che tipo di lavoro eseguirà e quale sarà il tipo di valore ritornato.

Una funzione – un’azione

Una funzione dovrebbe fare esattamente ciò che il suo nome descrive, niente di più.

Due azioni separate solitamente meritano due funzioni diverse, anche se molto spesso vengono chiamate insieme (in questo caso potrebbe essere utile creare una terza funzione che chiama entrambe).

Un paio di esempi che non rispettano queste regole:

  • getAge – sarebbe un pessimo nome se mostrasse un alert con l’età (dovrebbe solo restituirlo).
  • createForm – sarebbe un pessimo nome se modificasse il documento, aggiungendo il form (dovrebbe solo crearlo e restituirlo).
  • checkPermission – sarebbe un pessimo nome se mostrasse il messaggio access granted/denied (dovrebbe solo eseguire il controllo e ritornare il risultato).

Questi esempi assumono i significati comuni dei prefissi. Il loro significato dipende da voi e dal vostro team. E’ normale che il tuo codice abbia caratteristiche diverse, ma è fondamentale avere una ‘firma’ il cui prefisso sia sensato, che faccia capire cosa un determinato tipo di funzione può o non può fare. Tutte le funzioni che iniziano con lo stesso prefisso dovrebbero seguire determinate regole. E’ fondamentale che il team condivida queste informazioni.

Nomi di funzioni ultra-corti

Funzioni che vengono utilizzate molto spesso potrebbero avere nomi molto corti.

Ad esempio il framework jQuery definisce una funzione con $. La libreria Lodash ha nel core una funzione denominata _.

Queste sono eccezioni. Generalmente i nomi delle funzioni sono precisi e descrittivi.

Funzioni == Commenti

Le funzioni dovrebbero essere brevi ed eseguire un solo compito. Se invece risultano lunghe, forse varrebbe la pena spezzarle in funzioni più piccole. Qualche volta può non essere semplice seguire questa regola, anche se sarebbe la cosa migliore.

Una funzione separata non è solo semplice da testare e debuggare – la sua stessa esistenza è un commento!

Ad esempio, osserviamo le due funzioni showPrimes(n) sotto . Entrambe ritornano i numeri primi fino a n.

La prima versione utilizza le etichette:

function showPrimes(n) {
  nextPrime: for (let i = 2; i < n; i++) {

    for (let j = 2; j < i; j++) {
      if (i % j == 0) continue nextPrime;
    }

    alert( i ); // un primo
  }
}

La seconda variante utilizza un funzione addizionale isPrime(n) per testare se un numero rispetta la condizione di essere primo:

function showPrimes(n) {

  for (let i = 2; i < n; i++) {
    if (!isPrime(i)) continue;

    alert(i);  // un primo
  }
}

function isPrime(n) {
  for (let i = 2; i < n; i++) {
    if ( n % i == 0) return false;
  }
  return true;
}

La seconda variante è più semplice da capire, non vi pare? Invece di un pezzo di codice vediamo il nome dell’azione (isPrime). Talvolta le persone si riferiscono a questo tipo di codice come auto descrittivo.

Quindi, le funzioni possono essere create anche se non abbiamo intenzione di riutilizzarle. Infatti forniscono una struttura migliore al codice e lo rendono più leggibile.

Riepilogo

La dichiarazione di una funzione si scrive così:

function name(parameters, delimited, by, comma) {
  /* codice */
}
  • Valori passati ad una funzione come parametri vengono copiati in variabili locali.
  • Una funzione può avere accesso alle variabili esterne. Questo meccanismo funziona solo dall’interno verso l’esterno. Il codice esterno non può vedere le variabili locali ad una funzione.
  • Una funzione può ritornare un valore. Se non lo fa esplicitamente, questo sarà undefined.

Per rendere il codice pulito e più facile da leggere, è consigliabile utilizzare principalmente variabili locali e parametri di funzione, non variabili esterne.

E’ sempre più facile capire una funzione che accetta parametri, li lavora e ritorna un valore piuttosto di una funzione che non richiede parametri ma, come effetto collaterale, modifica variabili esterne.

Denominare le funzioni:

  • Un nome dovrebbe descrivere chiaramente ciò che una funzione fa. Quando vediamo la chiamata di una funzione nel codice, se questa ha un buon nome ci farà immediatamente capire il suo comportamento e cosa ritornerà.
  • Una funzione è un azione, quindi i nomi delle funzioni utilizzano molto spesso dei verbi.
  • Esistono molti prefissi comuni come create…, show…, get…, check… e molti altri. Utilizzateli per descrivere il comportamento di una funzione.

Le funzioni sono il principale blocco di un codice. Ora che abbiamo studiato le basi possiamo iniziare a crearle e utilizzarle. Ma questo è solo l’inizio. Ci ritorneremo molto spesso, andando molto in profondità, per capirne bene le caratteristiche.

Esercizi

importanza: 4

La seguente funzione ritorna true se il parametro age è maggiore di 18.

Altrimenti richiede una conferma e ritorna il risultato:

function checkAge(age) {
  if (age > 18) {
    return true;
  } else {
    // ...
    return confirm('Did parents allow you?');
  }
}

La funzione lavorerebbe diversamente se rimuovessimo else?

function checkAge(age) {
  if (age > 18) {
    return true;
  }
  // ...
  return confirm('Did parents allow you?');
}

Ci sono differenze nel comportamento di queste due varianti?

No, non ci sono differenze.

importanza: 4

La seguente funzione ritorna true se il parametro age è maggiore di 18.

Altrimenti richiede la conferma e ritorna il risultato.

function checkAge(age) {
  if (age > 18) {
    return true;
  } else {
    return confirm('Did parents allow you?');
  }
}

Riscrivila in modo che il comportamento sia uguale, ma senza utilizzare if. In una sola riga.

Fai due varianti di checkAge:

  1. Utilizzando l’operatore ?
  2. Utilizzando OR ||

Utilizzando l’operatore ?:

function checkAge(age) {
  return (age > 18) ? true : confirm('Did parents allow you?');
}

Utilizzando OR || (la variante più breve):

function checkAge(age) {
  return (age > 18) || confirm('Did parents allow you?');
}

Nota che le parentesi che includono age > 18 non sono obbligatorie. Vengono utilizzate per migliorare la leggibilità.

importanza: 1

Scrivi una funzione min(a,b) che ritorna il valore più piccolo tra due numeri a e b.

Ad esempio:

min(2, 5) == 2
min(3, -1) == -1
min(1, 1) == 1

Una soluzione utilizza if:

function min(a, b) {
  if (a < b) {
    return a;
  } else {
    return b;
  }
}

Un’altra soluzione, con l’operatore '?':

function min(a, b) {
  return a < b ? a : b;
}

P.S. Nel caso di uguaglianza a == b non ha importanza quale si ritorna.

importanza: 4

Scrivi una funzione pow(x,n) che ritorna la potenza n di x. In altre parole, moltiplica x per se stesso n volte e ritorna il risultato.

pow(3, 2) = 3 * 3 = 9
pow(3, 3) = 3 * 3 * 3 = 27
pow(1, 100) = 1 * 1 * ...* 1 = 1

Crea una pagina web che con un prompt richiede x e n, e in seguito mostra il risultato di pow(x,n).

Esegui la demo

P.S. In questo esercizio è sufficiente che per n la funzione supporti solo i numeri naturali interi, a partire da 1.

function pow(x, n) {
  let result = x;

  for (let i = 1; i < n; i++) {
    result *= x;
  }

  return result;
}

let x = prompt("x?", '');
let n = prompt("n?", '');

if (n < 1) {
  alert(`Power ${n} is not supported, use a positive integer`);
} else {
  alert( pow(x, n) );
}
Mappa del tutorial

Commenti

leggi questo prima di lasciare un commento…
  • Per qualsiasi suggerimento - per favore, apri una issue su GitHub o una pull request, piuttosto di lasciare un commento.
  • Se non riesci a comprendere quanto scitto nell'articolo – ti preghiamo di fornire una spiegazione chiara.
  • Per inserire delle righe di codice utilizza il tag <code>, per molte righe – includile nel tag <pre>, per più di 10 righe – utilizza una sandbox (plnkr, jsbin, codepen…)