Uso de iteradores y generadores en JavaScript para optimizar el código

Introducción

¿Alguna vez ha necesitado recorrer una lista, pero la operación tomó una cantidad de tiempo significativa para completar? ¿Alguna vez ha tenido un bloqueo del programa porque una operación usó demasiada memoria? Esto me sucedió cuando intenté implementar una función que genera números primos. 

Generar números primos de hasta un millón llevó mucho más tiempo del que me hubiera gustado. Pero generar números hasta 10 millones era imposible. Mi programa se bloqueaba o simplemente se colgaba. Ya estaba usando el tamiz de Eratóstenes, que se supone que es más eficiente para generar primos que el enfoque de fuerza bruta.

Si se encuentra en una situación similar, puede intentar usar un algoritmo diferente. Existen algoritmos de búsqueda y clasificación que funcionan mejor en entradas más grandes. El inconveniente es que esos algoritmos pueden ser más difíciles de entender de inmediato. Otra opción es utilizar un lenguaje de programación diferente.. 

Un lenguaje compilado puede ser capaz de procesar el código significativamente más rápido. Pero usar otro idioma puede no ser práctico. También puedes intentar usar múltiples hilos. Una vez más, puede que no sea práctico porque su lenguaje de programación tendría que admitir esto.

Afortunadamente, con JavaScript, hay otra opción. Si tiene una tarea de computación intensiva, puede usar iteradores y generadores para ganar algo de eficiencia. Los iteradores son propiedad de ciertas colecciones de JavaScript. 

Los iteradores mejoran la eficiencia al permitirle consumir los elementos en una lista de uno en uno como si fueran un flujo. Los generadores son un tipo especial de función que puede pausar la ejecución. Invocar un generador le permite producir datos un trozo a la vez sin la necesidad de almacenarlos primero en una lista.

Iteradores

Primero, revisemos las diferentes maneras en que puedes recorrer las colecciones en JavaScript. Un bucle de la forma. para (inicial; condición; paso) … ejecutará los comandos en su cuerpo un número específico de veces. De manera similar, un bucle while ejecutará los comandos en su cuerpo mientras su condición sea verdadera. 

Puede usar estos bucles para recorrer una lista incrementando una variable de índice en cada iteración. Una iteración es una ejecución del cuerpo de un bucle. Estos bucles no conocen la estructura de su lista. Actúan como contadores..

UNA para / en bucle y un para / de Los bucles están diseñados para iterar sobre estructuras de datos específicas. Iterar sobre una estructura de datos significa que está recorriendo cada uno de sus elementos. UNA para / en bucle itera sobre las claves en un objeto de JavaScript plano. UNA para / de bucle itera sobre los valores de un iterable. ¿Qué es un iterable? En pocas palabras, un iterable es un objeto que tiene un iterador. Ejemplos de iterables son matrices y conjuntos. El iterador es una propiedad del objeto que proporciona un mecanismo para atravesar el objeto..

Lo que hace especial a un iterador es cómo atraviesa una colección. Otros bucles deben cargar la colección completa por adelantado para iterar sobre ella, mientras que un iterador solo necesita conocer la posición actual en la colección. 

Usted accede al elemento actual llamando al siguiente método del iterador. El siguiente método devolverá el valor del elemento actual y un valor booleano para indicar cuándo ha llegado al final de la colección. El siguiente es un ejemplo de cómo crear un iterador a partir de una matriz.

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

También puede iterar sobre los valores del iterador usando una para / de lazo. Utilice este método cuando sepa que desea acceder a todos los elementos del objeto. Así es como utilizarías un bucle para recorrer la lista anterior:

para (const elem de él) console.log (elem); 

¿Por qué usarías un iterador? El uso de un iterador es beneficioso cuando el costo computacional de procesar una lista es alto. Si tiene un origen de datos que es muy grande, puede causar problemas en su programa si intenta iterar sobre él porque se debe cargar toda la colección.. 

Con un iterador, puede cargar los datos en trozos. Esto es más eficiente porque solo manipula la parte de la lista que necesita, sin incurrir en el costo adicional de procesar la lista completa. 

Un ejemplo podría ser que ha cargado datos de un archivo o base de datos, y desea mostrar progresivamente la información en la pantalla. Puede crear un iterador a partir de los datos y configurar un controlador de eventos para capturar algunos elementos cada vez que ocurra el evento. Este es un ejemplo de cómo podría ser una implementación de este tipo:

deja posts = carga (url); deja it = posts [Symbol.iterator] (); función loadPosts (iterable, count) for (let i = 0; i < count; i++)  display(iterable.next().value);   document.getElementById('btnNext').onclick = loadPosts(it, 5);

Generadores

Si desea crear una colección, puede hacerlo con un generador. Una función generadora puede devolver valores uno a uno al detener la ejecución en cada iteración. Cuando creas una instancia de un generador, se puede acceder a estos elementos utilizando un iterador. Esta es la sintaxis general para crear una función de generador:

function * genFunc () … valor de rendimiento; 

los * significa que esta es una función de generador. los rendimiento la palabra clave detiene nuestra función y proporciona el estado del generador en ese momento en particular. ¿Por qué usarías un generador? Usaría un generador cuando quiera producir un valor algorítmicamente en una colección. Es particularmente útil si tienes una colección muy grande o infinita. Veamos un ejemplo para entender cómo esto nos ayuda.. 

Supongamos que tiene un juego de billar en línea que ha creado y desea emparejar jugadores con salas de juegos. Tu objetivo es generar todas las formas en que puedes elegir dos jugadores distintos de tu lista de 2,000 jugadores. Las combinaciones de dos jugadores generadas a partir de la lista. ['a B C D'] sería ab, ac, anuncio, bc, bd, cd.  Esta es una solución usando bucles anidados:

función combos (lista) const n = list.length; dejar resultado = []; para (sea 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']));

Ahora intente ejecutar la función con una lista de 2,000 elementos. (Puedes inicializar tu lista usando un bucle for que agrega los números 1 a 2,000 a una matriz). ¿Qué pasa ahora cuando ejecutas tu código?? 

Cuando ejecuto el código en un editor en línea, la página web falla. Cuando lo pruebo en la consola en Chrome, puedo ver la salida imprimiendo lentamente. Sin embargo, la CPU de mi computadora comienza a sobrecargarse, y tengo que forzar la salida de Chrome. Este es el código revisado que usa una función de generador:

función * combos (lista) const n = list.length; para (sea 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(); 

Otro ejemplo es si quisiéramos generar los números en la secuencia de Fibonacci hasta el infinito. Aquí hay una implementación:

función * fibGen () dejar corriente = 0; dejar siguiente = 1; while (verdadero) rendimiento actual; dejar nextNum = current + next; actual = siguiente; next = nextNum;  deja que = fibGen (); it.next (). value; // 0 it.next (). Value; // 1 it.next (). Value; // 1 it.next (). Value; // 2

Normalmente, un bucle infinito bloqueará tu programa. los FibGen La función es capaz de ejecutarse para siempre porque no hay una condición de detención. Pero como es un generador, controlas cuando se ejecuta cada paso..

revisión

Los iteradores y generadores son útiles cuando se desea procesar una colección de manera incremental. Usted gana eficiencia al hacer un seguimiento del estado de la colección en lugar de todos los elementos de la colección. Los elementos de la colección se evalúan uno por uno y la evaluación del resto de la colección se retrasa hasta más tarde. 

Los iteradores proporcionan una manera eficiente de recorrer y manipular listas grandes. Los generadores proporcionan una manera eficiente de construir listas. Debe probar estas técnicas cuando, de lo contrario, utilizaría un algoritmo complejo o implementaría una programación paralela para optimizar su código..

Si está buscando recursos adicionales para estudiar o usar en su trabajo, vea lo que tenemos disponible en el mercado de Envato.

Aprender JavaScript: la guía completa

Hemos creado una guía completa para ayudarlo a aprender JavaScript, ya sea que esté comenzando a trabajar como desarrollador web o si desea explorar temas más avanzados..

Recursos

  • Patrones de diseño: elementos de software orientado a objetos reutilizables: patrón de iterador
  • Especificación ECMAScript para iteradores y generadores
  • Estructura e interpretación de programas de computadora - Capítulo 3.5: Streams