L’autore ha selezionato l’Open Internet/Free Speech Fund per ricevere una donazione come parte del programma Write for DOnations.
Introduzione
In ECMAScript 2015, i generatori sono stati introdotti nel linguaggio JavaScript. Un generatore è un processo che può essere messo in pausa e ripreso e può produrre più valori. Un generatore in JavaScript consiste in una funzione generatore, che restituisce un oggetto iterabile Generator
.
I generatori possono mantenere lo stato, fornendo un modo efficiente per creare iteratori, e sono in grado di gestire flussi di dati infiniti, che possono essere utilizzati per implementare lo scorrimento infinito sul frontend di un’applicazione web, per operare sui dati delle onde sonore, e altro ancora. Inoltre, quando vengono usati con le Promesse, i generatori possono imitare la funzionalità async/await
, che ci permette di trattare il codice asincrono in modo più diretto e leggibile. Anche se async/await
è un modo più prevalente per trattare i comuni e semplici casi d’uso asincroni, come il recupero di dati da un’API, i generatori hanno caratteristiche più avanzate che rendono utile imparare ad usarli.
In questo articolo, copriremo come creare funzioni generatore, come iterare su oggetti Generator
, la differenza tra yield
e return
all’interno di un generatore, e altri aspetti del lavoro con i generatori.
Funzioni generatrici
Una funzione generatrice è una funzione che restituisce un oggetto Generator
, ed è definita dalla parola chiave function
seguita da un asterisco (*
), come mostrato di seguito:
// Generator function declarationfunction* generatorFunction() {}
A volte, vedrete l’asterisco accanto al nome della funzione, al contrario della parola chiave funzione, come function *generatorFunction()
. Questo funziona allo stesso modo, ma function*
è una sintassi più ampiamente accettata.
Le funzioni generatrici possono anche essere definite in un’espressione, come le funzioni regolari:
// Generator function expressionconst generatorFunction = function*() {}
I generatori possono anche essere i metodi di un oggetto o di una classe:
Gli esempi in questo articolo useranno la sintassi della dichiarazione della funzione generatrice.
Nota: A differenza delle funzioni regolari, i generatori non possono essere costruiti con la parola chiave new
, né possono essere usati insieme alle funzioni freccia.
Ora che sai come dichiarare le funzioni generatrici, guardiamo gli oggetti iterabili Generator
che restituiscono.
Oggetti generatori
Tradizionalmente, le funzioni in JavaScript vengono eseguite fino al completamento, e la chiamata di una funzione restituisce un valore quando arriva alla parola chiave return
. Se la parola chiave return
viene omessa, una funzione restituirà implicitamente undefined
.
Nel seguente codice, per esempio, dichiariamo una funzione sum()
che restituisce un valore che è la somma di due argomenti interi:
// A regular function that sums two valuesfunction sum(a, b) { return a + b}
La chiamata della funzione restituisce un valore che è la somma degli argomenti:
const value = sum(5, 6) // 11
Una funzione generatrice, tuttavia, non restituisce un valore immediatamente, e invece restituisce un oggetto iterabile Generator
. Nell’esempio seguente, dichiariamo una funzione e le diamo un singolo valore di ritorno, come una funzione standard:
// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}
Quando invochiamo la funzione generatrice, essa restituirà l’oggetto Generator
, che possiamo assegnare a una variabile:
// Assign the Generator object to generatorconst generator = generatorFunction()
Se questa fosse una funzione regolare, ci aspetteremmo che generator
ci dia la stringa restituita nella funzione. Tuttavia, ciò che otteniamo effettivamente è un oggetto in uno stato suspended
. Chiamare generator
darà quindi un output simile al seguente:
L’oggetto Generator
restituito dalla funzione è un iteratore. Un iteratore è un oggetto che ha un metodo next()
disponibile, che è usato per iterare attraverso una sequenza di valori. Il metodo next()
restituisce un oggetto con value
e done
proprietà. value
rappresenta il valore restituito, e done
indica se l’iteratore ha percorso tutti i suoi valori o no.
Sapendo questo, chiamiamo next()
sul nostro generator
e otteniamo il valore corrente e lo stato dell’iteratore:
// Call the next method on the Generator objectgenerator.next()
Questo darà il seguente risultato:
Output{value: "Hello, Generator!", done: true}
Il valore restituito dalla chiamata next()
è Hello, Generator!
, e lo stato di done
è true
, perché questo valore proviene da un return
che ha chiuso l’iteratore. Poiché l’iteratore è finito, lo stato della funzione generatore cambierà da suspended
a closed
. Chiamando di nuovo generator
si otterrà quanto segue:
OutputgeneratorFunction {<closed>}
A partire da ora, abbiamo solo dimostrato come una funzione generatrice possa essere un modo più complesso per ottenere il valore return
di una funzione. Ma le funzioni generatrici hanno anche caratteristiche uniche che le distinguono dalle funzioni normali. Nella prossima sezione, impareremo l’operatore yield
e vedremo come un generatore può mettere in pausa e riprendere l’esecuzione.
Operatori yield
I generatori introducono una nuova parola chiave in JavaScript: yield
. yield
può mettere in pausa una funzione generatrice e restituire il valore che segue yield
, fornendo un modo leggero per iterare i valori.
In questo esempio, metteremo in pausa la funzione generatrice tre volte con valori diversi, e restituiremo un valore alla fine. Poi assegneremo il nostro oggetto Generator
alla variabile generator
.
Ora, quando chiamiamo next()
sulla funzione generatore, questa farà una pausa ogni volta che incontra yield
. done
sarà impostato su false
dopo ogni yield
, indicando che il generatore non ha finito. Una volta che incontra un return
, o non ci sono più yield
incontrati nella funzione, done
passerà a true
, e il generatore sarà finito.
Utilizzate il metodo next()
quattro volte di seguito:
// Call next four timesgenerator.next()generator.next()generator.next()generator.next()
Queste daranno le seguenti quattro righe di output in ordine:
Output{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}
Nota che un generatore non richiede un return
; se omesso, l’ultima iterazione restituirà {value: undefined, done: true}
, come qualsiasi successiva chiamata a next()
dopo che un generatore ha completato.
Iterare su un generatore
Utilizzando il metodo next()
, abbiamo iterato manualmente attraverso l’oggetto Generator
, ricevendo tutte le proprietà value
e done
dell’oggetto completo. Tuttavia, proprio come Array
, Map
e Set
, un Generator
segue il protocollo di iterazione, e può essere iterato con for...of
:
// Iterate over Generator objectfor (const value of generator) { console.log(value)}
Questo restituirà quanto segue:
OutputNeoMorpheusTrinity
L’operatore spread può anche essere usato per assegnare i valori di un Generator
ad un array.
// Create an array from the values of a Generator objectconst values = console.log(values)
Questo darà il seguente array:
Output(3)
Entrambi gli operatori spread e for...of
non fattorizzeranno il return
nei valori (in questo caso, sarebbe stato 'The Oracle'
).
Nota: Mentre entrambi questi metodi sono efficaci per lavorare con generatori finiti, se un generatore ha a che fare con un flusso di dati infinito, non sarà possibile usare direttamente spread o for...of
senza creare un ciclo infinito.
Chiude un generatore
Come abbiamo visto, un generatore può avere la sua proprietà done
impostata a true
e il suo stato impostato a closed
iterando attraverso tutti i suoi valori. Ci sono altri due modi per annullare immediatamente un generatore: con il metodo return()
e con il metodo throw()
.
Con return()
, il generatore può essere terminato in qualsiasi punto, proprio come se una dichiarazione return
fosse stata nel corpo della funzione. Potete passare un argomento in return()
, o lasciarlo vuoto per un valore non definito.
Per dimostrare return()
, creeremo un generatore con alcuni valori yield
ma nessun return
nella definizione della funzione:
function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()
Il primo next()
ci darà 'Neo'
, con done
impostato su false
. Se invochiamo un metodo return()
sull’oggetto Generator
subito dopo, otterremo il valore passato e done
impostato a true
. Qualsiasi ulteriore chiamata a next()
darà la risposta predefinita del generatore completato con un valore non definito.
Per dimostrarlo, eseguite i seguenti tre metodi su generator
:
generator.next()generator.return('There is no spoon!')generator.next()
Questo darà i tre seguenti risultati:
Output{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}
Il metodo return()
ha forzato l’oggetto Generator
a completare e a ignorare qualsiasi altra parola chiave yield
. Questo è particolarmente utile nella programmazione asincrona quando è necessario rendere le funzioni annullabili, come l’interruzione di una richiesta web quando un utente vuole eseguire un’altra azione, poiché non è possibile annullare direttamente una Promise.
Se il corpo di una funzione generatrice ha un modo per catturare e gestire gli errori, è possibile utilizzare il metodo throw()
per lanciare un errore nel generatore. Questo avvia il generatore, lancia l’errore e termina il generatore.
Per dimostrarlo, metteremo un try...catch
all’interno del corpo della funzione generatore e registreremo un errore se ne viene trovato uno:
Ora, eseguiremo il metodo next()
, seguito da throw()
:
generator.next()generator.throw(new Error('Agent Smith!'))
Questo darà il seguente output:
Output{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}
Utilizzando throw()
, abbiamo iniettato un errore nel generatore, che è stato catturato da try...catch
e registrato nella console.
Metodi e stati dell’oggetto generatore
La seguente tabella mostra una lista di metodi che possono essere usati sugli oggetti Generator
:
Metodo | Descrizione |
---|---|
next() |
Ritorna il prossimo valore in un generatore |
return() |
Ritorna un valore in un generatore e termina il generatore |
throw() |
Lancia un errore e termina il generatore |
La prossima tabella elenca i possibili stati di un oggetto Generator
:
Status | Descrizione |
---|---|
suspended |
Il generatore ha fermato l’esecuzione ma non è terminato |
closed |
Il generatore è terminato per un errore, ritornando, o iterando attraverso tutti i valori |
yield Delegation
In aggiunta al regolare operatore yield
, i generatori possono anche usare l’espressione yield*
per delegare ulteriori valori ad un altro generatore. Quando il yield*
viene incontrato all’interno di un generatore, andrà all’interno del generatore delegato e inizierà ad iterare attraverso tutti i yield
fino a quando quel generatore non sarà chiuso. Questo può essere usato per separare diverse funzioni del generatore per organizzare semanticamente il vostro codice, pur avendo tutti i loro yield
iterabili nel giusto ordine.
Per dimostrare, possiamo creare due funzioni generatrici, una delle quali opererà yield*
sull’altra:
Poi, iteriamo attraverso la funzione generatrice begin()
:
// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}
Questo darà i seguenti valori nell’ordine in cui sono generati:
Output1234
Il generatore esterno ha prodotto i valori 1
e 2
, poi ha delegato all’altro generatore con yield*
, che ha restituito 3
e 4
.
yield*
può anche delegare a qualsiasi oggetto che sia iterabile, come un Array o una Map. La delega del rendimento può essere utile nell’organizzazione del codice, poiché qualsiasi funzione all’interno di un generatore che volesse usare yield
dovrebbe essere anch’essa un generatore.
Flussi di dati infiniti
Uno degli aspetti utili dei generatori è la capacità di lavorare con flussi di dati e collezioni infinite. Questo può essere dimostrato creando un ciclo infinito all’interno di una funzione generatore che incrementa un numero di uno.
Nel seguente blocco di codice, definiamo questa funzione generatore e poi avviamo il generatore:
Ora, iteriamo attraverso i valori usando next()
:
// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()
Questo darà il seguente output:
Output{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}
La funzione restituisce valori successivi nel ciclo infinito mentre la proprietà done
rimane false
, assicurando che non finirà.
Con i generatori, non dovete preoccuparvi di creare un ciclo infinito, perché potete fermare e riprendere l’esecuzione a volontà. Tuttavia, dovete ancora fare attenzione a come invocate il generatore. Se usate spread o for...of
su un flusso di dati infinito, starete ancora iterando su un ciclo infinito tutto in una volta, il che causerà il crash dell’ambiente.
Per un esempio più complesso di un flusso di dati infinito, possiamo creare una funzione generatrice di Fibonacci. La sequenza di Fibonacci, che aggiunge continuamente i due valori precedenti, può essere scritta usando un ciclo infinito all’interno di un generatore come segue:
Per testare questo, possiamo fare un ciclo attraverso un numero finito e stampare la sequenza di Fibonacci sulla console.
// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}
Questo darà il seguente:
Output0112358132134
La capacità di lavorare con serie di dati infinite è una parte di ciò che rende i generatori così potenti. Questo può essere utile per esempi come l’implementazione dello scroll infinito sul frontend di un’applicazione web.
Passare valori nei generatori
In tutto questo articolo, abbiamo usato i generatori come iteratori, e abbiamo prodotto valori in ogni iterazione. Oltre a produrre valori, i generatori possono anche consumare valori da next()
. In questo caso, yield
conterrà un valore.
È importante notare che il primo next()
che viene chiamato non passerà un valore, ma avvierà solo il generatore. Per dimostrarlo, possiamo registrare il valore di yield
e chiamare next()
alcune volte con alcuni valori.
Questo darà il seguente output:
Output100200{value: "The end", done: true}
È anche possibile seminare il generatore con un valore iniziale. Nell’esempio seguente, faremo un ciclo for
e passeremo ogni valore nel metodo next()
, ma passeremo anche un argomento alla funzione iniziale:
Recupereremo il valore da next()
e daremo un nuovo valore alla prossima iterazione, che è il valore precedente per dieci. Questo darà il seguente risultato:
Output010203040
Un altro modo di affrontare l’avvio di un generatore è quello di avvolgere il generatore in una funzione che chiamerà sempre next()
una volta prima di fare qualsiasi altra cosa.
async/await con i generatori
Una funzione asincrona è un tipo di funzione disponibile in ES6+ JavaScript che rende il lavoro con dati asincroni più facile da capire facendolo sembrare sincrono. I generatori hanno una serie più ampia di capacità rispetto alle funzioni asincrone, ma sono in grado di replicare un comportamento simile. Implementare la programmazione asincrona in questo modo può aumentare la flessibilità del tuo codice.
In questa sezione, dimostreremo un esempio di riproduzione di async
/await
con i generatori.
Costruiamo una funzione asincrona che usa l’API Fetch per ottenere dati dall’API JSONPlaceholder (che fornisce dati JSON di esempio a scopo di test) e registra la risposta nella console.
Iniziamo definendo una funzione asincrona chiamata getUsers
che recupera dati dall’API e restituisce un array di oggetti, quindi chiamiamo getUsers
:
Questo darà dati JSON simili ai seguenti:
Utilizzando i generatori, possiamo creare qualcosa di quasi identico che non usa le parole chiave async
/await
. Invece, userà una nuova funzione creata da noi e i valori yield
invece delle promesse await
.
Nel seguente blocco di codice, definiamo una funzione chiamata getUsers
che usa la nostra nuova funzione asyncAlt
(che scriveremo in seguito) per imitare async
/await
.
Come possiamo vedere, sembra quasi identico all’implementazione async
/await
, eccetto che c’è una funzione generatrice che viene passata e che restituisce dei valori.
Ora possiamo creare una funzione asyncAlt
che assomiglia ad una funzione asincrona. asyncAlt
ha una funzione generatrice come parametro, che è la nostra funzione che produce le promesse che fetch
restituisce. asyncAlt
restituisce essa stessa una funzione, e risolve ogni promessa che trova fino all’ultima:
Questo darà lo stesso output della versione async
/await
:
Nota che questa implementazione è per dimostrare come i generatori possono essere usati al posto di async
/await
, e non è un progetto pronto per la produzione. Non ha la gestione degli errori impostata, né la possibilità di passare parametri nei valori prodotti. Anche se questo metodo può aggiungere flessibilità al tuo codice, spesso async/await
sarà una scelta migliore, poiché astrae i dettagli di implementazione e ti permette di concentrarti sulla scrittura di codice produttivo.
Conclusion
I generatori sono processi che possono fermare e riprendere l’esecuzione. Sono una caratteristica potente e versatile di JavaScript, anche se non sono comunemente usati. In questo tutorial, abbiamo imparato a conoscere le funzioni generatore e gli oggetti generatore, i metodi disponibili ai generatori, gli operatori yield
e yield*
, e i generatori usati con insiemi di dati finiti e infiniti. Abbiamo anche esplorato un modo per implementare codice asincrono senza callback annidati o lunghe catene di promesse.
Se vuoi imparare di più sulla sintassi JavaScript, dai un’occhiata ai nostri tutorial Capire questo, legare, chiamare e applicare in JavaScript e Capire gli oggetti mappa e set in JavaScript.