Rajesh Babu

Follow

4 maggio, 2018 – 9 min read

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.

Generatore Turbine

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…)

Funzione generatore di base

Nota: Nell’esempio precedente la funzione generatrice a cui si accede direttamente senza un wrapper viene sempre eseguita solo fino alla prima resa. Quindi, per definizione è necessario assegnare il generatore a una variabile per iterare correttamente su di essa.

Ciclo di vita di una funzione generatrice

Prima di procedere oltre, diamo una rapida occhiata al diagramma a blocchi del ciclo di vita della funzione generatrice:

Ciclo di vita di una funzione generatrice

Ogni volta che si incontra una resa, la funzione generatrice restituisce un oggetto contenente il valore della resa incontrata e lo stato fatto. Allo stesso modo, quando si incontra un ritorno, si ottiene il valore di ritorno e anche lo stato di fatto come vero. Ogni volta che lo stato di done viene restituito come true, significa essenzialmente che la funzione generatore ha completato la sua esecuzione, e non è possibile nessun’altra resa.

Tutto ciò che viene dopo il primo ritorno viene ignorato, incluse altre espressioni di resa.

Leggi di più per capire meglio lo schema a blocchi.

Assegnare la resa ad una variabile

Nel precedente esempio di codice abbiamo visto un’introduzione alla creazione di un generatore di base con una resa. E abbiamo ottenuto l’output previsto. Ora, supponiamo di assegnare l’intera espressione di rendimento ad una variabile nel codice sottostante.

Assegnando il rendimento ad una variabile

Qual è il risultato dell’intera espressione di rendimento passata alla variabile? Niente o Indefinito …

Perché? A partire dalla seconda next(), la resa precedente viene sostituita con gli argomenti passati nella funzione successiva. Dal momento che non passiamo nulla qui nel metodo next, si presume che l’intera ‘espressione di resa precedente’ sia indefinita.

Con questo in mente, saltiamo alla prossima sezione per capire di più sul passaggio di argomenti al metodo next().

Passaggio di argomenti al metodo next()

Con riferimento al diagramma a blocchi sopra, parliamo del passaggio di argomenti nella funzione next. Questa è una delle parti più complicate dell’intera implementazione del generatore.

Consideriamo il seguente pezzo di codice, dove il rendimento è assegnato a una variabile, ma questa volta passiamo un valore nel metodo next().

Guardiamo il codice qui sotto nella console. E la spiegazione subito dopo.

Passaggio di argomenti al metodo next()

Spiegazione:

  1. Quando chiamiamo il primo next(20), viene stampata ogni riga di codice fino alla prima resa. Poiché non abbiamo alcuna espressione di rendimento precedente, il valore 20 viene scartato. Nell’output otteniamo il valore di rendimento come i*10, che qui è 100. Anche lo stato dell’esecuzione si ferma con la prima resa e la const j non è ancora impostata.
  2. La seconda chiamata next(10), sostituisce l’intera prima espressione di resa con 10, immaginate la resa (i * 10) = 10, che continua a impostare il valore della const j a 50 prima di restituire il valore della seconda resa. Il valore di rendimento qui è 2 * 50 / 4 = 25.
  3. Il terzo next(5), sostituisce l’intero secondo rendimento con 5, portando il valore di k a 5. E continua ad eseguire l’istruzione return e restituisce (x + y + z) => (10 + 50 + 5) = 65 come valore finale di rendimento insieme a done true.

Questo potrebbe essere un po’ travolgente per chi legge per la prima volta, ma prendetevi 5 minuti buoni per leggerlo più e più volte per capirlo completamente.

Passare Yield come argomento di una funzione

Ci sono n-numeri di casi d’uso che circondano yield riguardo a come può essere usato all’interno di un generatore di funzioni. Diamo un’occhiata al codice qui sotto per uno di questi utilizzi interessanti di yield, insieme alla spiegazione.

Rendi come argomento di una funzione

Spiegazione

  1. Il primo next() produce un valore indefinito perché l’espressione yield non ha valore.
  2. La seconda next() produce “I am usless”, il valore che è stato passato. E prepara l’argomento per la chiamata della funzione.
  3. 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.

Rendimenti chiamando una funzione

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.

Yield with promises

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.

Basic yield*

Spiegazione

  1. La prima chiamata next() produce un valore di 1.
  2. 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.
  3. 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.

Rendimento* con ritorno

Spiegazione

  1. Nella prima next() andiamo direttamente al rendimento 1 e restituiamo il suo valore.
  2. La seconda next() restituisce 2.
  3. La terza next(), restituisce ‘foo’ e va avanti a restituire ‘the end’, assegnando ‘foo’ al risultato const lungo il percorso.
  4. 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.

Yield over built-in iterables

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.

Rendimenti con for…of

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:

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.