Una funzione è un blocco parametrico di codice definito una volta e chiamato più volte in seguito. In JavaScript una funzione è composta e influenzata da molti componenti:
- Codice JavaScript che forma il corpo della funzione
- L’elenco dei parametri
- Le variabili accessibili dallo scope lessicale
- Il valore restituito
- Il contesto
this
quando la funzione viene invocata - Nominata o una funzione anonima
- La variabile che contiene l’oggetto della funzione
-
arguments
oggetto (o mancante in una funzione freccia)
Questo post vi insegna sei approcci per dichiarare funzioni JavaScript: la sintassi, gli esempi e le insidie comuni. Inoltre, capirete quando usare uno specifico tipo di funzione in determinate circostanze.
1. Dichiarazione di funzione
Una dichiarazione di funzione è composta da
function
parola chiave, seguita da un nome di funzione obbligatorio, una lista di parametri in una coppia di parentesi(para1, ..., paramN)
e una coppia di parentesi graffe{...}
che delimita il codice del corpo.
Un esempio di dichiarazione di funzione:
// function declarationfunction isEven(num) { return num % 2 === 0;}isEven(24); // => trueisEven(11); // => false
function isEven(num) {...}
è una dichiarazione di funzione che definisce la funzione isEven
che determina se un numero è pari.
La dichiarazione di funzione crea una variabile nello scope corrente con l’identificatore uguale al nome della funzione. Questa variabile contiene l’oggetto della funzione.
La variabile della funzione è issata in cima allo scope corrente, il che significa che la funzione può essere invocata prima della dichiarazione (vedi questo capitolo per maggiori dettagli).
La funzione creata ha un nome, il che significa che la proprietà name
dell’oggetto funzione contiene il suo nome. È utile quando si visualizza lo stack delle chiamate: nel debug o nella lettura dei messaggi di errore.
Vediamo queste proprietà in un esempio:
// Hoisted variableconsole.log(hello('Aliens')); // => 'Hello Aliens!'// Named functionconsole.log(hello.name) // => 'hello'// Variable holds the function objectconsole.log(typeof hello); // => 'function'function hello(name) { return `Hello ${name}!`;}
La dichiarazione della funzione function hello(name) {...}
crea una variabile hello
che viene portata in cima allo scope corrente. La variabile hello
contiene l’oggetto funzione e hello.name
contiene il nome della funzione: 'hello'
.
1.1 Una funzione regolare
La dichiarazione di funzione corrisponde ai casi in cui è necessaria una funzione regolare. Regolare significa che si dichiara la funzione una volta e successivamente la si invoca in molti posti diversi. Questo è lo scenario di base:
function sum(a, b) { return a + b;}sum(5, 6); // => 11().reduce(sum) // => 10
Perché la dichiarazione di funzione crea una variabile nello scope corrente, insieme alle chiamate di funzioni regolari, è utile per la ricorsione o il distacco di ascoltatori di eventi. Contrariamente alle espressioni di funzioni o alle funzioni freccia, che non creano un legame con la variabile della funzione con il suo nome.
Per esempio, per calcolare ricorsivamente il fattoriale bisogna accedere alla funzione all’interno:
function factorial(n) { if (n === 0) { return 1; } return n * factorial(n - 1);}factorial(4); // => 24
Inside factorial()
viene fatta una chiamata ricorsiva utilizzando la variabile che contiene la funzione: factorial(n - 1)
.
È possibile utilizzare un’espressione di funzione e assegnarla a una variabile regolare, per esempio const factorial = function(n) {...}
. Ma la dichiarazione di funzione function factorial(n)
è compatta (non c’è bisogno di const
e =
).
Un’importante proprietà della dichiarazione di funzione è il suo meccanismo di sollevamento. Permette di usare la funzione prima della dichiarazione nello stesso ambito.
L’hoisting è utile in alcune situazioni. Per esempio, quando si vuole vedere come viene chiamata la funzione all’inizio di uno script, senza leggere l’implementazione della funzione. L’implementazione della funzione può trovarsi in basso nel file, quindi si può anche non scorrere lì.
Puoi leggere maggiori dettagli sull’issaggio della dichiarazione di funzione qui.
1.2 Differenza dall’espressione di funzione
È facile confondere la dichiarazione di funzione e l’espressione di funzione. Sembrano molto simili ma producono funzioni con proprietà diverse.
Una regola facile da ricordare: la dichiarazione di funzione in una dichiarazione inizia sempre con la parola chiave function
. Altrimenti è un’espressione di funzione (vedi 2.).
L’esempio seguente è una dichiarazione di funzione in cui la dichiarazione inizia con la parola chiave function
:
// Function declaration: starts with "function"function isNil(value) { return value == null;}
In caso di espressioni di funzione la dichiarazione JavaScript non inizia con la parola chiave function
(è presente da qualche parte nel mezzo del codice della dichiarazione):
// Function expression: starts with "const"const isTruthy = function(value) { return !!value;};// Function expression: an argument for .filter()const numbers = ().filter(function(item) { return typeof item === 'number';});// Function expression (IIFE): starts with "("(function messageFunction(message) { return message + ' World!';})('Hello');
1.3 Dichiarazione di funzione nei condizionali
Alcuni ambienti JavaScript possono lanciare un errore di riferimento quando si invoca una funzione la cui dichiarazione appare all’interno dei blocchi {...}
di if
for
o while
dichiarazioni.
Abilitiamo la modalità strict e vediamo cosa succede quando una funzione è dichiarata in un condizionale:
(function() { 'use strict'; if (true) { function ok() { return 'true ok'; } } else { function ok() { return 'false ok'; } } console.log(typeof ok === 'undefined'); // => true console.log(ok()); // Throws "ReferenceError: ok is not defined"})();
Quando si chiama ok()
, JavaScript lancia ReferenceError: ok is not defined
, perché la dichiarazione della funzione è dentro un blocco condizionale.
La dichiarazione di funzione nei condizionali è consentita in modalità non rigorosa, il che rende il tutto ancora più confuso.
Come regola generale per queste situazioni, quando una funzione deve essere creata da condizioni – usa un’espressione di funzione. Vediamo come è possibile:
(function() { 'use strict'; let ok; if (true) { ok = function() { return 'true ok'; }; } else { ok = function() { return 'false ok'; }; } console.log(typeof ok === 'function'); // => true console.log(ok()); // => 'true ok'})();
Perché la funzione è un oggetto regolare, assegnatela a una variabile a seconda della condizione. Invocare ok()
funziona bene, senza errori.
2. Espressione di funzione
Un’espressione di funzione è determinata da una parola chiave
function
, seguita da un nome di funzione opzionale, una lista di parametri in una coppia di parentesi(para1, ..., paramN)
e una coppia di parentesi graffe{ ... }
che delimita il codice del corpo.
Alcuni esempi di espressione di funzione:
const count = function(array) { // Function expression return array.length;}const methods = { numbers: , sum: function() { // Function expression return this.numbers.reduce(function(acc, num) { // func. expression return acc + num; }); }}count(); // => 3methods.sum(); // => 14
L’espressione di funzione crea un oggetto funzione che può essere usato in diverse situazioni:
- Assegnato ad una variabile come oggetto
count = function(...) {...}
- Crea un metodo su un oggetto
sum: function() {...}
- Utilizzare la funzione come callback
.reduce(function(...) {...})
L’espressione di funzione è il cavallo di battaglia di JavaScript. Di solito, si ha a che fare con questo tipo di dichiarazione di funzione, insieme alla funzione freccia (se si preferisce la sintassi breve e il contesto lessicale).
2.1 Espressione di funzione con nome
Una funzione è anonima quando non ha un nome (la proprietà name
è una stringa vuota ''
):
( function(variable) {return typeof variable; }).name; // => ''
Questa è una funzione anonima il cui nome è una stringa vuota.
A volte il nome della funzione può essere dedotto. Per esempio, quando l’anonimo è assegnato ad una variabile:
const myFunctionVar = function(variable) { return typeof variable; };myFunctionVar.name; // => 'myFunctionVar'
Il nome della funzione anonima è 'myFunctionVar'
, perché il nome della variabile myFunctionVar
è usato per dedurre il nome della funzione.
Quando l’espressione ha il nome specificato, questa è un’espressione di funzione nominata. Ha alcune proprietà aggiuntive rispetto alla semplice espressione di funzione:
- Si crea una funzione nominata, cioè
name
proprietà contiene il nome della funzione - All’interno del corpo della funzione una variabile con lo stesso nome contiene l’oggetto della funzione
Utilizziamo l’esempio precedente, ma impostiamo un nome nell’espressione di funzione:
const getType = function funName(variable) { console.log(typeof funName === 'function'); // => true return typeof variable;}console.log(getType(3)); // => 'number'console.log(getType.name); // => 'funName'console.log(typeof funName); // => 'undefined'
function funName(variable) {...}
è un’espressione di funzione con nome. La variabile funName
è accessibile all’interno dello scopo della funzione, ma non all’esterno. In entrambi i casi, la proprietà name
dell’oggetto funzione contiene il nome: funName
.
2.2 Favorire un’espressione di funzione con nome
Quando un’espressione di funzione const fun = function() {}
è assegnata a una variabile, alcuni motori deducono il nome della funzione da questa variabile. Tuttavia, i callback potrebbero essere passati come espressioni di funzione anonime, senza memorizzazione nelle variabili: quindi il motore non può determinare il suo nome.
È ragionevole favorire le funzioni con nome ed evitare quelle anonime per ottenere benefici come:
- I messaggi di errore e gli stack delle chiamate mostrano informazioni più dettagliate quando si usano i nomi delle funzioni
- Un debug più comodo riducendo il numero di nomi di stack anonimi
- Il nome della funzione dice cosa fa la funzione
- Si può accedere alla funzione all’interno del suo scope per chiamate ricorsive o per staccare gli ascoltatori di eventi
3. Definizione di metodo shorthand
La definizione di metodo shorthand può essere usata in una dichiarazione di metodo su letterali di oggetto e classi ES2015. Potete definirli usando un nome di funzione, seguito da una lista di parametri in una coppia di parentesi
(para1, ..., paramN)
e una coppia di parentesi graffe{ ... }
che delimita le dichiarazioni del corpo.
L’esempio seguente usa una definizione di metodo stenografico in un letterale di oggetto:
const collection = { items: , add(...items) { this.items.push(...items); }, get(index) { return this.items; }};collection.add('C', 'Java', 'PHP');collection.get(1) // => 'Java'
add()
e get()
metodi nell’oggetto collection
sono definiti usando la definizione breve del metodo. Questi metodi sono chiamati come al solito: collection.add(...)
e collection.get(...)
.
L’approccio breve della definizione del metodo ha diversi vantaggi rispetto alla tradizionale definizione della proprietà con un nome, due punti :
e un’espressione di funzione add: function(...) {...}
:
- Una sintassi più breve è più facile da capire
- La definizione sintetica del metodo crea una funzione con nome, al contrario di un’espressione di funzione. È utile per il debug.
La sintassi class
richiede dichiarazioni di metodo in forma breve:
class Star { constructor(name) { this.name = name; } getMessage(message) { return this.name + message; }}const sun = new Star('Sun');sun.getMessage(' is shining') // => 'Sun is shining'
3. Nomi di proprietà e metodi calcolati1 Nomi di proprietà e metodi calcolati
ECMAScript 2015 aggiunge una bella caratteristica: i nomi di proprietà calcolati nei letterali di oggetto e nelle classi.
Le proprietà calcolate usano una sintassi leggermente diversa () {...}
, quindi la definizione del metodo appare così:
const addMethod = 'add', getMethod = 'get';const collection = { items: , (...items) { this.items.push(...items); }, (index) { return this.items; }};collection('C', 'Java', 'PHP');collection(1) // => 'Java'
(...) {...}
e (...) {...}
sono dichiarazioni sintetiche di metodi con nomi di proprietà calcolate.
4. Funzione freccia
Una funzione freccia è definita utilizzando una coppia di parentesi che contiene la lista dei parametri
(param1, param2, ..., paramN)
, seguita da una freccia grassa=>
e una coppia di parentesi graffe{...}
che delimita le dichiarazioni del corpo.
Quando la funzione freccia ha un solo parametro, la coppia di parentesi può essere omessa. Quando contiene una sola dichiarazione, anche le parentesi graffe possono essere omesse.
Vediamo l’uso di base della funzione freccia:
const absValue = (number) => { if (number < 0) { return -number; } return number;}absValue(-10); // => 10absValue(5); // => 5
absValue
è una funzione freccia che calcola il valore assoluto di un numero.
La funzione dichiarata utilizzando una fat arrow ha le seguenti proprietà:
- La funzione freccia non crea il suo contesto di esecuzione, ma lo prende lessicalmente (contrariamente all’espressione di funzione o alla dichiarazione di funzione, che creano il proprio
this
a seconda dell’invocazione) - La funzione freccia è anonima. Tuttavia, il motore può dedurre il suo nome dalla variabile che contiene la funzione.
-
arguments
oggetto non è disponibile nella funzione freccia (contrariamente ad altri tipi di dichiarazione che fornisconoarguments
oggetto). Siete liberi di usare i parametri di resto(...params)
, però.
4.1 Trasparenza del contesto
this
parola chiave è un aspetto confuso di JavaScript (controllare questo articolo per una spiegazione dettagliata su this
).
Perché le funzioni creano il proprio contesto di esecuzione, spesso è difficile rilevare this
valore.
ECMAScript 2015 migliora l’uso di this
introducendo la funzione freccia, che prende il contesto lessicalmente (o semplicemente usa this
dall’ambito esterno immediato). Questo è bello perché non dovete usare .bind(this)
o memorizzare il contesto var self = this
quando una funzione ha bisogno del contesto racchiuso.
Vediamo come this
viene ereditato dalla funzione esterna:
class Numbers { constructor(array) { this.array = array; } addNumber(number) { if (number !== undefined) { this.array.push(number); } return (number) => { console.log(this === numbersObject); // => true this.array.push(number); }; }}const numbersObject = new Numbers();const addMethod = numbersObject.addNumber();addMethod(1);addMethod(5);console.log(numbersObject.array); // =>
Numbers
classe tiene un array di numeri e fornisce un metodo addNumber()
per inserire nuovi numeri.
Quando addNumber()
viene chiamato senza argomenti, viene restituita una chiusura che permette di inserire i numeri. Questa chiusura è una funzione freccia che ha this
come istanza numbersObject
perché il contesto è preso lessicalmente dal metodo addNumbers()
.
Senza la funzione freccia, dovete fissare manualmente il contesto. Significa usare dei workaround come il metodo .bind()
:
//... return function(number) { console.log(this === numbersObject); // => true this.array.push(number); }.bind(this);//...
o memorizzare il contesto in una variabile separata var self = this
:
//... const self = this; return function(number) { console.log(self === numbersObject); // => true self.array.push(number); };//...
La trasparenza del contesto può essere usata quando si vuole mantenere this
così com’è, preso dal contesto che lo racchiude.
4.2 Callback brevi
Quando si crea una funzione freccia, le coppie di parentesi e le parentesi graffe sono opzionali per un singolo parametro e una singola dichiarazione del corpo. Questo aiuta a creare funzioni di callback molto brevi.
Facciamo una funzione che trovi se un array contiene 0
:
const numbers = ;numbers.some(item => item === 0); // => true
item => item === 0
è una funzione freccia che sembra semplice.
Nota che le funzioni freccia brevi annidate sono difficili da leggere. Il modo conveniente per usare la forma più breve della funzione freccia è un singolo callback (senza annidamento).
Se necessario, usate la sintassi estesa delle funzioni freccia quando scrivete funzioni freccia annidate. È semplicemente più facile da leggere.
5. Funzione generatore
La funzione generatore in JavaScript restituisce un oggetto generatore. La sua sintassi è simile all’espressione di funzione, alla dichiarazione di funzione o alla dichiarazione di metodo, solo che richiede un carattere stella *
.
La funzione generatrice può essere dichiarata nelle seguenti forme:
a. Forma di dichiarazione della funzione function* <name>()
:
function* indexGenerator(){ var index = 0; while(true) { yield index++; }}const g = indexGenerator();console.log(g.next().value); // => 0console.log(g.next().value); // => 1
b. Modulo di espressione di funzione function* ()
:
const indexGenerator = function* () { let index = 0; while(true) { yield index++; }};const g = indexGenerator();console.log(g.next().value); // => 0console.log(g.next().value); // => 1
c. Forma stenografica di definizione del metodo *<name>()
:
const obj = { *indexGenerator() { var index = 0; while(true) { yield index++; } }}const g = obj.indexGenerator();console.log(g.next().value); // => 0console.log(g.next().value); // => 1
In tutti e 3 i casi la funzione generatore ritorna l’oggetto generatore g
. Successivamente g
viene usato per generare serie di numeri incrementati.
6. Un’altra cosa: new Function
In JavaScript le funzioni sono oggetti di prima classe – una funzione è un oggetto regolare di tipo function
.
I modi di dichiarazione descritti sopra creano lo stesso tipo di oggetto funzione. Vediamo un esempio:
function sum1(a, b) { return a + b;}const sum2 = function(a, b) { return a + b;}const sum3 = (a, b) => a + b;console.log(typeof sum1 === 'function'); // => trueconsole.log(typeof sum2 === 'function'); // => trueconsole.log(typeof sum3 === 'function'); // => true
Il tipo oggetto funzione ha un costruttore: Function
.
Quando Function
viene invocato come costruttore new Function(arg1, arg2, ..., argN, bodyString)
, viene creata una nuova funzione. Gli argomenti arg1, args2, ..., argN
passati al costruttore diventano i nomi dei parametri della nuova funzione e l’ultimo argomento bodyString
viene usato come codice del corpo della funzione.
Creiamo una funzione che somma due numeri:
const numberA = 'numberA', numberB = 'numberB';const sumFunction = new Function(numberA, numberB, 'return numberA + numberB');sumFunction(10, 15) // => 25
sumFunction
creata con Function
costruttore ha i parametri numberA
e numberB
e il corpo return numberA + numberB
.
Le funzioni create in questo modo non hanno accesso all’ambito corrente, quindi le chiusure non possono essere create. Sono sempre create nell’ambito globale.
Una possibile applicazione di new Function
è un modo migliore per accedere all’oggetto globale in un browser o in uno script NodeJS:
(function() { 'use strict'; const global = new Function('return this')(); console.log(global === window); // => true console.log(this === window); // => false})();
Ricordate che le funzioni non dovrebbero quasi mai essere dichiarate usando new Function()
. Poiché il corpo della funzione viene valutato a runtime, questo approccio eredita molti problemi di utilizzo eval()
: rischi per la sicurezza, debug più difficile, nessun modo di applicare le ottimizzazioni del motore, nessun editor a completamento automatico.
7. Alla fine, quale modo è migliore?
Non c’è un vincitore o un perdente. La decisione su quale tipo di dichiarazione scegliere dipende dalla situazione.
Ci sono però alcune regole che si possono seguire nelle situazioni comuni.
Se la funzione usa this
dalla funzione di chiusura, la funzione freccia è una buona soluzione. Quando la funzione di callback ha una dichiarazione breve, anche la funzione freccia è una buona opzione, perché crea un codice breve e leggero.
Per una sintassi più breve quando si dichiarano metodi su letterali di oggetto, la dichiarazione di metodo shorthand è preferibile.
new Function
modo di dichiarare le funzioni normalmente non dovrebbe essere usato. Principalmente perché apre potenziali rischi di sicurezza, non permette il completamento automatico del codice negli editor e perde le ottimizzazioni del motore.