Capire i generatori in JavaScript

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:

Output
generatorFunction {<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:

Output
NeoMorpheusTrinity

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:

Output
1234

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:

Output
0112358132134

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:

Output
100200{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:

Output
010203040

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.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.