Rajesh Babu

Follow

May 4, 2018 – 9 min read

.

Si has sido un desarrollador de JavaScript durante los últimos dos o cinco años, seguramente te habrás encontrado con posts que hablan de Generadores e Iteradores. Mientras que los Generadores y los Iteradores están inherentemente asociados, los Generadores parecen un poco más intimidantes que el otro.

Turbina de Generadores

Los Iteradores son una implementación de objetos Iterables como mapas, arrays y cadenas que nos permite iterar sobre ellos utilizando next(). Tienen una amplia variedad de casos de uso a través de Generadores, Observables y operadores de propagación.

Recomiendo el siguiente enlace para aquellos de ustedes que son nuevos a los iteradores, Guía de Iteradores.

Para comprobar si su objeto se ajusta al protocolo iterable, verifique el uso de la incorporada Symbol.iterator:

Los generadores introducidos como parte de ES6 no han sufrido ningún cambio para las versiones posteriores de JavaScript y están aquí para quedarse más tiempo. Quiero decir, ¡mucho tiempo! Así que no hay que huir de ello. Aunque ES7 y ES8 tienen algunas actualizaciones nuevas, no tienen la misma magnitud de cambio que tuvo ES6 con respecto a ES5, que llevó a JavaScript al siguiente nivel, por así decirlo.

Al final de este post, estoy seguro de que tendrás una sólida comprensión de cómo funcionan los Generadores de funciones. Si eres un profesional, por favor ayúdame a mejorar el contenido añadiendo tus comentarios en las respuestas. Por si acaso, si tienes dificultades para seguir el código, también he añadido la explicación de la mayor parte del código para ayudarte a entender mejor.

Las funciones en JavaScript, como todos sabemos, «se ejecutan hasta el retorno/final». Las Funciones Generadoras por otro lado, «se ejecutan hasta yield/return/end». A diferencia de las funciones normales las Funciones Generadoras una vez llamadas, devuelven el Objeto Generador, que contiene todo el Iterable Generador que puede ser iterado usando el método next() o el bucle for…of.

Cada llamada a next() en el generador ejecuta cada línea de código hasta el siguiente yield que encuentra y suspende su ejecución temporalmente.

Sintácticamente se identifican con un *, ya sea función* X o función *X, – ambas significan lo mismo.

Una vez creada, la llamada a la función generadora devuelve el Objeto Generador. Este objeto generador necesita ser asignado a una variable para llevar la cuenta de los subsiguientes métodos next() llamados sobre sí mismo. Si el generador no se asigna a una variable entonces siempre rendirá sólo hasta la primera expresión yield en cada next().

Las funciones generadoras se construyen normalmente utilizando expresiones yield. Cada yield dentro de la función generadora es un punto de parada antes de que comience el siguiente ciclo de ejecución. Cada ciclo de ejecución se desencadena mediante el método next() del generador.

En cada llamada a next(), la expresión yield devuelve su valor en forma de objeto que contiene los siguientes parámetros.

{ value: 10, done: false } // assuming that 10 is the value of yield

  • Valor – es todo lo que se escribe a la derecha de la palabra clave yield, puede ser una llamada a una función, un objeto o prácticamente cualquier cosa. Para los rendimientos vacíos este valor es indefinido.
  • Done – indica el estado del generador, si se puede ejecutar más o no. Cuando done devuelve true, significa que la función ha terminado su ejecución.

(Si crees que está un poco por encima de tu cabeza, lo tendrás más claro cuando veas el ejemplo de abajo…)

Función generadora básica

Nota: En el ejemplo anterior la función generadora a la que se accede directamente sin envoltura siempre se ejecuta sólo hasta el primer yield. Por lo tanto, por definición es necesario asignar el Generador a una variable para iterar correctamente sobre él.

Ciclo de vida de una función generadora

Antes de continuar, echemos un vistazo al diagrama de bloques del ciclo de vida de la función generadora:

Ciclo de vida de una Función Generadora

Cada vez que se encuentra un yield la función generadora devuelve un objeto que contiene el valor del yield encontrado y el estado realizado. Del mismo modo, cuando se encuentra un retorno, obtenemos el valor del retorno y también el estado hecho como verdadero. Siempre que el estado hecho se devuelva como verdadero, significa esencialmente que la función generadora ha completado su ejecución, y no es posible ningún otro yield.

Todo lo que sucede después del primer return se ignora, incluyendo otras expresiones de yield.

Lee más para entender mejor el diagrama de bloques.

Asignación de Yield a una variable

En el ejemplo de código anterior vimos una introducción a la creación de un generador básico con un yield. Y obtuvimos la salida esperada. Ahora, supongamos que asignamos toda la expresión yield a una variable en el código siguiente.

Asignando yield a una variable

¿Cuál es el resultado de toda la expresión yield pasada a la variable? Nada o Indefinido …

¿Por qué? A partir del segundo next(), el yield anterior se sustituye por los argumentos pasados en la siguiente función. Dado que no pasamos nada aquí en el método next, se asume que toda la ‘expresión de rendimiento anterior’ como indefinida.

Con esto en mente, vamos a saltar a la siguiente sección para entender más sobre el paso de argumentos al método next().

Pasando argumentos al método next()

Con referencia al diagrama de bloques anterior, vamos a hablar de pasar argumentos a la función next. Esta es una de las partes más complicadas de toda la implementación del generador.

Consideremos el siguiente trozo de código, donde el yield se asigna a una variable, pero esta vez pasamos un valor en el método next().

Miremos el código de abajo en la consola. Y la explicación justo después.

Pasando argumentos a la función next()

Explicación:

  1. Cuando llamamos al primer next(20), se imprimen todas las líneas de código hasta el primer yield. Como no tenemos ninguna expresión yield anterior este valor 20 se descarta. En la salida obtenemos el valor de yield como i*10, que aquí es 100. También el estado de la ejecución se detiene con el primer yield y la const j todavía no está establecida.
  2. La segunda llamada next(10), reemplaza toda la primera expresión yield con 10, imagina yield (i * 10) = 10, que pasa a establecer el valor de la const j a 50 antes de devolver el valor del segundo yield. El valor de yield aquí es 2 * 50 / 4 = 25.
  3. El tercer next(5), reemplaza todo el segundo yield con 5, llevando el valor de k a 5. Y continúa ejecutando la sentencia return y devuelve (x + y + z) => (10 + 50 + 5) = 65 como valor final de yield junto con done true.

Esto puede ser un poco abrumador para los lectores de primera vez, pero tómate unos buenos 5 minutos para leerlo una y otra vez para entenderlo completamente.

Pasando Yield como un argumento de una función

Hay n-número de casos de uso que rodean a yield con respecto a cómo se puede utilizar dentro de un generador de funciones. Veamos el código siguiente para uno de esos usos interesantes de yield, junto con la explicación.

Yield como argumento de una función

Explicación

  1. El primer next() produce un valor indefinido porque la expresión yield no tiene valor.
  2. El segundo next() devuelve «I am usless», el valor que se pasó. Y prepara el argumento para la llamada a la función.
  3. La tercera next(), llama a la función con un argumento indefinido. Como se mencionó anteriormente, el método next() llamado sin ningún argumento significa esencialmente que toda la expresión yield anterior es indefinida. Por lo tanto, esto imprime undefined y termina la ejecución.

Yield con una llamada a una función

Aparte de devolver valores yield también puede llamar a funciones y devolver el valor o imprimir el mismo. Veamos el código de abajo y entendamos mejor.

Yield llamando a una función

El código de arriba devuelve el obj de retorno de la función como valor yield. Y termina la ejecución estableciendo undefined al usuario const.

Rendimiento con promesas

El rendimiento con promesas sigue el mismo enfoque que la llamada a la función anterior, en lugar de devolver un valor de la función, devuelve una promesa que puede ser evaluada posteriormente para el éxito o el fracaso. Veamos el código de abajo para entender cómo funciona.

Yield with promises

El apiCall devuelve las promesas como el valor yield, cuando se resuelve después de 2 segundos imprime el valor que necesitamos.

Yield*

Hasta ahora hemos estado viendo los casos de uso de la expresión yield, ahora vamos a ver otra expresión llamada yield*. Yield* cuando se utiliza dentro de una función generadora delega a otra función generadora. En pocas palabras, completa sincrónicamente la función generadora en su expresión antes de pasar a la siguiente línea.

Veamos el código y la explicación de abajo para entender mejor. Este código está sacado de los docs de la web MDN.

Basic yield*

Explicación

  1. La primera llamada a next() produce un valor de 1.
  2. La segunda llamada a next(), sin embargo, es una expresión yield*, lo que significa intrínsecamente que vamos a completar otra función generadora especificada en la expresión yield* antes de continuar con la función generadora actual.
  3. En su mente, puede suponer que el código anterior se sustituye como el de abajo
function* g2() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}

Este pasará a terminar la ejecución del generador. Sin embargo, hay una capacidad distinta del yield* que debes tener en cuenta mientras usas return, en la siguiente sección.

Yield* con Return

Yield* con return se comporta un poco diferente al yield* normal. Cuando yield* se utiliza con una sentencia return se evalúa a ese valor, lo que significa que toda la función yield*() se hace igual al valor devuelto por la función generadora asociada.

Veamos el código y la explicación de abajo para entenderlo mejor.

Yield* con return

Explicación

  1. En el primer next() vamos directamente al yield 1 y devolvemos su valor.
  2. En la segunda next() se devuelve 2.
  3. En la tercera next(), se devuelve ‘foo’ y se pasa a rendir el ‘fin’, asignando ‘foo’ al resultado de la const en el camino.
  4. El último next() termina la ejecución.

Yield* con un objeto iterable incorporado

Hay otra propiedad interesante de yield* que vale la pena mencionar, similar al valor de retorno, yield* también puede iterar sobre objetos iterables como Array, String y Map.

Veamos cómo funciona en tiempo real.

Yield sobre iterables incorporados

Aquí en el código yield* itera sobre cada posible objeto iterable que se le pasa como expresión. Supongo que el código en sí se explica por sí mismo.

Mejores prácticas

Además de todo esto, cada iterador/generador puede ser iterado sobre un bucle for…of. De forma similar a nuestro método next() que se llama explícitamente, el bucle for…of pasa internamente a la siguiente iteración basándose en la palabra clave yield. Y sólo itera hasta el último yield y no procesa las declaraciones de retorno como el método next().

Puedes comprobar lo mismo en el código de abajo.

Rendimiento con for…of

El valor de retorno final no se imprime, porque el bucle for…of itera sólo hasta el último rendimiento. Por lo tanto, entra dentro de las mejores prácticas evitar las declaraciones de retorno dentro de una función generadora, ya que afectaría a la reutilización de la función cuando se itera sobre un for…of.

Conclusión

Espero que esto cubra los casos de uso básicos de las funciones generadoras y espero sinceramente que haya dado una mejor comprensión de cómo funcionan los generadores en JavaScript ES6 y superiores. Si te gusta mi contenido por favor deja 1, 2, 3 o incluso 50 palmadas :).

Por favor, sígueme en mi cuenta de GitHub para más proyectos de JavaScript y Full-Stack:

Deja una respuesta

Tu dirección de correo electrónico no será publicada.