Se sei stato uno sviluppatore JavaScript negli ultimi due o cinque anni ti sarai sicuramente imbattuto in post che parlano di Generatori e Iteratori. Mentre i Generatori e gli Iteratori sono intrinsecamente associati, i Generatori sembrano un po’ più intimidatori degli altri.
Gli Iteratori sono un’implementazione di oggetti Iterable come mappe, array e stringhe che ci permette di iterare su di essi usando next(). Hanno un’ampia varietà di casi d’uso attraverso generatori, osservabili e operatori Spread.
Consiglio il seguente link per quelli di voi che sono nuovi agli iteratori, Guide to Iterators.
Per controllare se il vostro oggetto è conforme al protocollo iterabile, verificate usando il built-in Symbol.iterator:
I generatori introdotti come parte di ES6 non hanno subito alcun cambiamento per le ulteriori versioni di JavaScript e sono qui per rimanere più a lungo. Intendo davvero a lungo! Quindi non c’è da scappare. Anche se ES7 e ES8 hanno alcuni nuovi aggiornamenti, non hanno la stessa grandezza di cambiamento che ES6 ha avuto da ES5, che ha portato JavaScript al livello successivo, per così dire.
Per la fine di questo post, sono sicuro che avrete una solida comprensione di come funzionano i Generatori di funzioni. Se sei un professionista, per favore aiutami a migliorare il contenuto aggiungendo i tuoi commenti nelle risposte. Nel caso in cui abbiate difficoltà a seguire il codice, ho anche aggiunto delle spiegazioni per la maggior parte del codice per aiutarvi a capire meglio.
Le funzioni in JavaScript, come tutti sappiamo, “girano fino al ritorno/alla fine”. Le funzioni generatrici, d’altra parte, “funzionano fino a quando non vengono rese/ritornate/fine”. A differenza delle normali funzioni le Generator Functions una volta chiamate, restituiscono l’oggetto Generator, che contiene l’intero Iterable Generator che può essere iterato usando il metodo next() o for…of loop.
Ogni chiamata next() sul generatore esegue ogni linea di codice fino al prossimo yield che incontra e sospende temporaneamente la sua esecuzione.
Sintatticamente sono identificati con un *, o funzione* X o funzione *X, – entrambi significano la stessa cosa.
Una volta creata, la chiamata alla funzione generatore restituisce l’oggetto generatore. Questo oggetto generatore deve essere assegnato a una variabile per tenere traccia dei successivi metodi next() chiamati su se stesso. Se il generatore non è assegnato ad una variabile, allora cederà sempre e solo fino alla prima espressione di resa su ogni next().
Le funzioni generatore sono normalmente costruite usando espressioni di resa. Ogni rendimento all’interno della funzione generatrice è un punto di arresto prima che inizi il prossimo ciclo di esecuzione. Ogni ciclo di esecuzione è innescato per mezzo del metodo next() sul generatore.
Ad ogni chiamata next(), l’espressione yield restituisce il suo valore sotto forma di un oggetto contenente i seguenti parametri.
{ value: 10, done: false } // assuming that 10 is the value of yield
- Valore – è tutto ciò che è scritto sul lato destro della parola chiave yield, può essere una chiamata di funzione, oggetto o praticamente qualsiasi cosa. Per i rendimenti vuoti questo valore è indefinito.
- Done – indica lo stato del generatore, se può essere eseguito ulteriormente o no. Quando done ritorna true, significa che la funzione ha finito la sua esecuzione.
(Se ti sembra che sia un po’ troppo complicato, ti sarà più chiaro quando vedrai l’esempio qui sotto…)
Spiegazione
- Il primo next() produce un valore indefinito perché l’espressione yield non ha valore.
- La seconda next() produce “I am usless”, il valore che è stato passato. E prepara l’argomento per la chiamata della funzione.
- La terza next(), chiama la funzione con un argomento indefinito. Come detto sopra, il metodo next() chiamato senza alcun argomento significa essenzialmente che l’intera espressione di rendimento precedente è indefinita. Quindi, questo stampa undefined e termina l’esecuzione.
Rendimento con una chiamata di funzione
Oltre a restituire valori, yield può anche chiamare funzioni e restituire il valore o stampare lo stesso. Guardiamo il codice qui sotto e capiamo meglio.
Il codice qui sopra restituisce l’obj di ritorno della funzione come valore di rendimento. E termina l’esecuzione impostando undefined all’utente const.
Rendimento con le promesse
Il rendimento con le promesse segue lo stesso approccio della chiamata di funzione sopra, invece di restituire un valore dalla funzione, restituisce una promessa che può essere valutata ulteriormente per successo o fallimento. Guardiamo il codice qui sotto per capire come funziona.
L’apiCall restituisce le promesse come valore di rendimento, quando viene risolto dopo 2 secondi stampa il valore che ci serve.
Rendimento*
Finora abbiamo visto i casi d’uso delle espressioni di rendimento, ora vedremo un’altra espressione chiamata yield*. Yield* quando è usata all’interno di una funzione generatrice delega un’altra funzione generatrice. In poche parole, completa sincronicamente la funzione generatrice nella sua espressione prima di passare alla riga successiva.
Guardiamo il codice e la spiegazione qui sotto per capire meglio. Questo codice è tratto dalla documentazione web MDN.
Spiegazione
- La prima chiamata next() produce un valore di 1.
- La seconda chiamata next(), tuttavia, è un’espressione yield*, che intrinsecamente significa che stiamo andando a completare un’altra funzione generatrice specificata nell’espressione yield* prima di continuare la funzione generatrice corrente.
- Nella vostra mente, potete assumere che il codice sopra sia sostituito come quello sotto
function* g2() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
Questo andrà a finire l’esecuzione del generatore. Tuttavia, c’è una capacità distinta di yield* che dovreste tenere a mente mentre usate return, nella prossima sezione.
Yield* con Return
Yield* con un return si comporta un po’ diversamente dal normale yield*. Quando yield* è usato con una dichiarazione di ritorno valuta a quel valore, il che significa che l’intera funzione yield*() diventa uguale al valore restituito dalla funzione generatrice associata.
Guardiamo il codice e la spiegazione qui sotto per capire meglio.
Spiegazione
- Nella prima next() andiamo direttamente al rendimento 1 e restituiamo il suo valore.
- La seconda next() restituisce 2.
- La terza next(), restituisce ‘foo’ e va avanti a restituire ‘the end’, assegnando ‘foo’ al risultato const lungo il percorso.
- L’ultima next() termina l’esecuzione.
Yield* con un oggetto iterabile incorporato
C’è un’altra interessante proprietà di yield* che merita di essere menzionata, simile al valore di ritorno, yield* può anche iterare su oggetti iterabili come Array, String e Map.
Guardiamo come funziona in tempo reale.
Qui nel codice lo yield* itera su ogni possibile oggetto iterabile che viene passato come espressione. Credo che il codice stesso sia autoesplicativo.
Best Practices
In cima a tutto questo, ogni iteratore/generatore può essere iterato su un ciclo for…of. Simile al nostro metodo next() che viene chiamato esplicitamente, il ciclo for…of passa internamente all’iterazione successiva in base alla parola chiave yield. E itera solo fino all’ultima resa e non elabora le dichiarazioni di ritorno come il metodo next().
Si può verificare lo stesso nel codice qui sotto.
Il valore di ritorno finale non viene stampato, perché il ciclo for…of itera solo fino all’ultimo rendimento. Quindi, rientra nella migliore pratica evitare dichiarazioni di ritorno all’interno di una funzione generatrice, poiché influenzerebbe la riusabilità della funzione quando si itera su un for…of.
Conclusione
Spero che questo copra i casi d’uso di base delle funzioni generatore e spero sinceramente che abbia dato una migliore comprensione di come funzionano i generatori in JavaScript ES6 e superiori. Se vi piace il mio contenuto lasciate 1, 2, 3 o anche 50 applausi :).
Seguitemi sul mio account GitHub per altri progetti JavaScript e Full-Stack: