Utilizzo di iteratori e generatori in JavaScript per ottimizzare il codice

introduzione

Hai mai avuto bisogno di scorrere un elenco, ma l'operazione ha richiesto molto tempo per essere completata? Hai mai avuto un arresto anomalo del programma perché un'operazione ha utilizzato troppa memoria? Questo è successo a me quando ho provato a implementare una funzione che genera numeri primi. 

Generare numeri primi fino a un milione ha richiesto molto più tempo di quanto avrei voluto. Ma generare numeri fino a 10 milioni era impossibile. Il mio programma si bloccherebbe o semplicemente si bloccherebbe. Stavo già usando il setaccio di Eratostene, che dovrebbe essere più efficiente nel generare i numeri primi che l'approccio della forza bruta.

Se ti trovi in ​​una situazione simile, puoi provare a utilizzare un algoritmo diverso. Esistono algoritmi di ricerca e ordinamento che funzionano meglio su input più grandi. Il rovescio della medaglia è che quegli algoritmi potrebbero essere più difficili da capire subito. Un'altra opzione è usare un linguaggio di programmazione diverso. 

Un linguaggio compilato potrebbe essere in grado di elaborare il codice in modo significativamente più rapido. Ma usare un'altra lingua potrebbe non essere pratico. Puoi anche provare a utilizzare più thread. Ancora una volta, potrebbe non essere pratico perché il tuo linguaggio di programmazione dovrebbe supportarlo.

Fortunatamente, con JavaScript, c'è un'altra opzione. Se si dispone di un'attività computazionalmente intensiva, è possibile utilizzare iteratori e generatori per ottenere un po 'di efficienza. Gli iteratori sono una proprietà di determinate raccolte JavaScript. 

Gli iteratori migliorano l'efficienza consentendo di consumare gli elementi di un elenco uno alla volta come se fossero uno stream. I generatori sono un tipo speciale di funzione che può sospendere l'esecuzione. Il richiamo di un generatore consente di produrre dati un chunk alla volta senza la necessità di memorizzarli prima in un elenco.

iteratori

Innanzitutto, esaminiamo i diversi modi in cui puoi scorrere le raccolte in JavaScript. Un ciclo del modulo for (initial; condition; step) ... eseguirà i comandi nel suo corpo un numero specifico di volte. Allo stesso modo, un ciclo while eseguirà i comandi nel suo corpo fino a quando la sua condizione è vera. 

È possibile utilizzare questi cicli per attraversare un elenco incrementando una variabile di indice ad ogni iterazione. Un'iterazione è un'esecuzione del corpo di un ciclo. Questi loop non conoscono la struttura del tuo elenco. Agiscono come contatori.

UN per / a ciclo e a per / di loop sono progettati per iterare su strutture dati specifiche. L'iterazione su una struttura dati significa che stai attraversando ciascuno dei suoi elementi. UN per / a loop esegue iterazioni sulle chiavi in ​​un oggetto JavaScript semplice. UN per / di loop itera sopra i valori di un iterabile. Cos'è un iterable? In poche parole, un iterabile è un oggetto che ha un iteratore. Esempi di iterables sono array e set. L'iteratore è una proprietà dell'oggetto che fornisce un meccanismo per attraversare l'oggetto.

Ciò che rende speciale un iteratore è come attraversa una collezione. Altri loop devono caricare l'intera raccolta in anticipo per iterare su di essa, mentre un iteratore deve solo conoscere la posizione corrente nella raccolta. 

Si accede all'attuale voce chiamando il prossimo metodo dell'iteratore. Il prossimo metodo restituirà il valore dell'elemento corrente e un valore booleano per indicare quando hai raggiunto la fine della raccolta. Di seguito è riportato un esempio di creazione di un iteratore da un array.

const alpha = ['a', 'b', 'c']; const it = alpha [Symbol.iterator] (); it.next (); // value: 'a', done: false it.next (); // value: 'b', done: false it.next (); // value: 'c', done: false it.next (); // value: undefined, done: true

È inoltre possibile eseguire un'iterazione sui valori dell'iteratore utilizzando a per / di ciclo continuo. Utilizzare questo metodo quando si sa che si desidera accedere a tutti gli elementi nell'oggetto. Questo è il modo in cui useresti un ciclo per scorrere l'elenco precedente:

for (const elem of it) console.log (elem); 

Perché dovresti usare un iteratore? L'utilizzo di un iteratore è vantaggioso quando il costo computazionale dell'elaborazione di un elenco è elevato. Se si dispone di un'origine dati molto grande, potrebbe causare problemi nel programma se si tenta di eseguire iterazioni su di esso perché l'intera raccolta deve essere caricata. 

Con un iteratore, puoi caricare i dati in blocchi. Questo è più efficiente perché si manipola solo la parte dell'elenco di cui si ha bisogno, senza incorrere nel costo aggiuntivo di elaborazione dell'intero elenco. 

Un esempio potrebbe essere il caricamento dei dati da un file o database e la visualizzazione progressiva delle informazioni sullo schermo. È possibile creare un iteratore dai dati e impostare un gestore di eventi per afferrare alcuni elementi ogni volta che si verifica l'evento. Questo è un esempio di come potrebbe essere una tale implementazione:

lascia posts = load (url); let it = posts [Symbol.iterator] (); function loadPosts (iterable, count) for (let i = 0; i < count; i++)  display(iterable.next().value);   document.getElementById('btnNext').onclick = loadPosts(it, 5);

generatori

Se vuoi creare una collezione, puoi farlo con un generatore. Una funzione generatore può restituire i valori uno alla volta sospendendo l'esecuzione ad ogni iterazione. Quando si crea un'istanza di un generatore, è possibile accedere a questi elementi utilizzando un iteratore. Questa è la sintassi generale per la creazione di una funzione generatore:

function * genFunc () ... yield value; 

Il * significa che questa è una funzione generatore. Il dare la precedenza parola chiave mette in pausa la nostra funzione e fornisce lo stato del generatore in quel particolare momento. Perché dovresti usare un generatore? Dovresti usare un generatore quando vuoi produrre algoritmicamente un valore in una collezione. È particolarmente utile se hai una collezione molto grande o infinita. Diamo un'occhiata a un esempio per capire come questo ci aiuta. 

Supponiamo che tu abbia un gioco di biliardo online che hai costruito e che desideri abbinare i giocatori alle sale giochi. Il tuo obiettivo è generare tutti i modi in cui puoi scegliere due giocatori distinti dalla tua lista di 2.000 giocatori. Le combinazioni a due giocatori generate dalla lista ['a', 'b', 'c', 'd'] sarebbe ab, ac, ad, bc, bd, cd.  Questa è una soluzione che utilizza cicli nidificati:

function combos (list) const n = list.length; let result = []; for (let i = 0; i < n - 1; i++)  for (let j = i + 1; j < n; j++)  result.push([list[i], list[j]]);   return result;  console.log(combos(['a', 'b', 'c', 'd']));

Ora prova ad eseguire la funzione con un elenco di 2000 elementi. (È possibile inizializzare l'elenco utilizzando un ciclo for che aggiunge i numeri da 1 a 2.000 a un array). Cosa succede ora quando esegui il tuo codice? 

Quando eseguo il codice in un editor online, la pagina Web si arresta in modo anomalo. Quando lo provo nella console di Chrome, riesco a vedere l'output stampare lentamente. Tuttavia, la CPU del mio computer inizia ad andare in overdrive e devo forzare l'uscita da Chrome. Questo è il codice revisionato utilizzando una funzione generatore:

function * combos (list) const n = list.length; for (let i = 0; i < n - 1; i++)  for (let j = i + 1; j < n; j++)  yield [list[i], list[j]];    let it = combos(['a', 'b', 'c', 'd']); it.next(); 

Un altro esempio è se volessimo generare i numeri nella sequenza di Fibonacci fino all'infinito. Ecco una implementazione:

function * fibGen () let current = 0; let next = 1; while (true) yield current; let nextNum = current + next; current = next; next = nextNum;  let it = fibGen (); . It.next () valore; // 0 it.next (). Value; // 1 it.next (). Value; // 1 it.next (). Value; // 2

Normalmente, un ciclo infinito farà crashare il tuo programma. Il fibGen la funzione è in grado di funzionare per sempre perché non ci sono condizioni di arresto. Ma dal momento che è un generatore, tu controlli quando ogni passo viene eseguito.

Revisione

Iteratori e generatori sono utili quando si desidera elaborare una raccolta in modo incrementale. Ottieni efficienza mantenendo traccia dello stato della raccolta anziché di tutti gli elementi nella raccolta. Gli elementi nella raccolta vengono valutati uno alla volta e la valutazione del resto della raccolta viene ritardata fino a un momento successivo. 

Gli iteratori forniscono un modo efficace per attraversare e manipolare elenchi di grandi dimensioni. I generatori forniscono un modo efficiente per creare elenchi. Dovresti provare queste tecniche quando altrimenti dovresti usare un algoritmo complesso o implementare una programmazione parallela per ottimizzare il tuo codice.

Se stai cercando ulteriori risorse da studiare o da utilizzare nel tuo lavoro, dai un'occhiata a ciò che abbiamo a disposizione nel mercato Envato.

Scopri JavaScript: la guida completa

Abbiamo creato una guida completa per aiutarti a imparare JavaScript, sia che tu stia appena iniziando come sviluppatore web o che desideri esplorare argomenti più avanzati.

risorse

  • Pattern di progettazione: elementi del software riutilizzabile orientato agli oggetti: pattern Iterator
  • Specifica ECMAScript per Iteratori e Generatori
  • Struttura e interpretazione dei programmi per computer - Capitolo 3.5: Flussi