JavaScript — Symbols & Generadores ES6 (Parte III)
Temario
- Introducción
- Metaprogramación
- Symbols (locales y globales, Símbolos conocidos[iterator, match, toPrimitive, otros])
- Generadores (Funciones generadoras)
i. Introducción
Vamos por la tercera parte de ES6 (parte I[ref], parte II[ref]) en la entrega pasada vimos nuevos métodos que hace que sea más simple programar.
En esta tercera entrega vamos a ver conceptos muy interesantes, y cosas que se han agregado, que estoy seguro, van a cambiar mucho la forma de programar en el futuro (si no es que ya lo están haciendo)…
ii. Metaprogramación
Antes de ver los siguientes temas, debemos entender, que es la metaprogramación y por que es muy importante en JavaScript.
El concepto realmente no es nuevo, de hecho, muchos lenguajes lo utilizan (Lip, Scala, Haskell, hasta JavaScript!), es probable que ya lo hayas utilizado sin darte cuenta…
Veamos que dice wikipedia:
La metaprogramación es una técnica de programación en la cual los programas de computadora tienen la capacidad de tratar a otros programas como sus datos. [ref]
Entonces, podemos decir, que es cuando creamos un código que lea, modifique, o que inclusive pueda generar código… la metaprogramación, la podemos dividir en los siguientes tipos:
- Generación de código: Como su nombre lo dice, es poder generar código. Con JavaScript desde la versión ES1, tenemos uno de ellos (muy odiado por cierto) que es el famoso eval (se encargaba de generar código, quizás de mala manera, pero cumple con el objetivo).
- Reflexión: Alterar el funcionamiento interno de una aplicación. Con JavaScript lo podemos encontrar en las funciones (name, bin, call, apply) y todos los métodos disponibles de Object (Object.getOwnProperties).
- Reflexión/Introspección: Es cuando no altera la estructura, solo recopilan información. Con JavaScript podemos encontrarlo en typeof, instanceof.
Con JavaScript ES6 podemos hacer metaprogramación con:
- Symbol: Tiene que ver con reflexión, ya que altera el funcionamiento interno de las clases u objetos.
- Reflect: Tiene que ver con reflexión/introspección, ya que nos ayuda a obtener información de muy bajo nivel sobre el código.
- Proxy: Tiene que ver con la reflexión/intercesión, va a envolver los objetos e interceptar sus comportamientos a través de trampas.
Perfecto, ya sabemos de manera básica que es la metaprogramación y sus diferentes tipos, teniendo los conceptos anteriores en mente, ya podemos continuar con el siguiente tema…
iii. Symbols
Sabemos qué Symbol[ref] es un tipo de dato primitivo, que se ha sido agregado en la edición de ECMAScript2015 (ES6).
Symbol es un tipo de datos cuyos valores son únicos e inmutables, donde los valores, son utilizados como claves de las propiedades de los objetos; y tenemos tres tipos:
- Locales (Symbol)
- Globales (Symbol.for)
- Símbolos conocidos
a — Locales (Symbol)
La sintaxis para crear Symbols de manera local, es la siguiente:
Veamos cómo se utiliza en el siguiente ejemplo:
Importante: Symbol es tipo de dato primitivo, por lo que anteponer el operador new, nos va arrojar un error.
Como hemos mencionado, la función Symbol acepta un parámetro como identificador y como bien mencionamos la descripción no se puede utilizar, pero es útil al depurar nuestra aplicación, veamos el siguiente ejemplo:
Como podemos observar en nuestro ejemplo:
- En la línea 5, vemos que usamos la variable n y nos regresa el valor correctamente (Mauricio)
- En la línea 6, vemos que nos imprime Symbol(name), que es el que hemos creado y el nombre que le asignamos.
- En la línea 7, al imprimir en consola todo el objeto, nos damos cuenta que la clave (key) para obtener el nombre es Symbol(name).
- En la línea 8, si intentamos acceder con el nombre que le asignamos a Symbol, nos arroja un error
Es importante saber que cada vez que creamos un nuevo Symbol, es único e irrepetible (sin importar que la descripción sea la misma), veamos el siguiente ejemplo:
Podemos observar en nuestro ejemplo, que hemos creado dos instancias diferentes de Symbol, aunque tengan el mismo nombre, y es por eso que al compararlos nos da false.
Otro punto importante a tomar en cuenta es que los Symbols son “invisibles” para los loops y otros métodos; veamos los siguientes ejemplos:
Da la impresión de que podemos usar Symbols para crear propiedades privadas ¿no?… aún es posible acceder a esas propiedades:
- Con Reflect.ownKeys obtenemos todas las keys (normales y Symbol)
- Con Object.getOwnPropertySymbols obtenemos las keys que son con Symbol
Veamos el siguiente ejemplo:
b — Globales (Symbol.for)
Tenemos otra forma de crear Symbols, y que realmente sean iguales, y es con el método .for.
La sintaxis para crear Symbols de manera global, es la siguiente:
No hay que olvidar que Symbol es un dato tipo primitivo (no importa que tenga métodos) ya que es inmutable.
Veamos un ejemplo:
c — Símbolos conocidos
ECMAScript2015 tiene símbolos predefinidos llamados símbolos conocidos, que son comportamientos comunes, que anteriormente se consideraban operaciones sólo internas.[ref]
Algunos ejemplos de estos símbolos son: Symbol.replace, Symbol.search, Symbol.iterator y Symbol.split.
Al ser símbolos globales, están expuestos, por lo que podemos hacer cambios para que los métodos principales llamen a nuestras funciones personalizadas, en lugar de las internas.
Veamos algunos de ellos:
c-1 — Symbol.iterator:
Es el protocolo iterador[ref] le permite a los objetos de JavaScript definir o personalizar su iteración, nativos como Array, Map o dentro de una sentencia for..of; para poder usarlo, su cadena prototípica debe tener una propiedad con un identificador Symbol.iterator.
Su sintaxis es la siguiente:
Entonces:
- Siempre que lo ocupemos, debe ser un método [Symbol.iterator] sin argumentos.
- [Symbol.iterator] va a retornar un objeto con la propiedad de tipo función llamada next.
- La función next, va a retornar un objeto con dos valores done y value.
- Cuando se encuentra el siguiente valor iterable, done es false y value es el valor iterable.
- Cuando se llega al último valor iterable y/o cumple con la condición, done es true.
Veamos primero un ejemplo, de for … of (recordemos que ya lo vimos anteriormente Loops[ref]):
Ahora hagamos lo mismo, pero con un objeto personalizable…
Veamos el código (no olvides darle al botón verde de play):
Hemos generado nuestro fakeFor, en el ejemplo anterior lo iteramos de forma manual, esto es, usando el next() vamos a automatizarlo un poco usando el for…of, veamos el código (no olvides darle al botón verde de play):
Podemos observar que el for..of, utiliza nuestro fakeFor para iterar.
Hasta aquí quizás pareciera que no tiene alguna utilidad, veamos algunos ejemplos más reales:
Ejemplo 1: Imaginemos que tenemos un arreglo de números y lo que queremos es iterar de atrás hacia adelante, quizás, lo más lógico sería usar el for (pero con el podemos cometer el error de Off by one error[ref]), o usar el método del Array.reverse[ref](pero recordemos que este modifica el arreglo y quizás no sea lo que queremos).
Veamos el código (no olvides darle al botón verde de play):
Con el ejemplo anterior tenemos la ventaja de que no modificamos el arreglo original, evitamos cometer errores comunes con el for, y además es ligeramente más rápido, inclusive podemos hacer uso del operador spread …[ref] sin necesidad de iterar.
Genial! ¿no?
Ejemplo 2: Supongamos que tenemos una lista de canciones, donde, cada vez que le demos click al botón de siguiente, nos va a mostrar una canción de manera aleatoria.
Primero debemos tener una lista de canciones (tomada de spotify[ref]):
Nuestro iterador randomSong:
Lo que hacemos es tener nuestro arreglo original, donde lo vamos a mandar al método shuffle, para que nos regrese un nuevo arreglo con las canciones en orden random, para que al momento de ser iterada tome dicho arreglo y no el original.
Y por último, la acción del botón next:
Se que no hemos visto los eventos(**), pero no se preocupen en otra story lo veremos con mucho detalle, por ahora solo concéntrese en la línea 5.
Veamos el código completo (no olvides darle al botón verde de play):
c-2 — Symbol.match
Un método que iguala a un String, también usado para determinar si un objeto puede ser usado como una expresión regular y es utilizado por String.prototype.match()[ref].
En nuestro ejemplo, podemos implementar alguna validación diferente en vez de una expresión regular (no olvides darle al botón verde de play):
Veamos paso a paso:
- Invocamos la función password, donde le pasamos un dato tipo string (que será nuestro password).
- El motor de JavaScript analiza el siguiente código: ‘otherpassword’.match(pwd)
- otherpassword se convierte en un objeto tipo String (new String(otherpassword))
- El método match llama internamente al método Symbol.match del objeto pwd, algo así: pwd[Symbol.match](‘otherpassword’)
- pwd[Symbol.match](‘otherpassword’) ejecuta la validación que hicimos, y regresa un resultado (para el primer ejemplo false)
c-3 — Symbol.toPrimitive
Un método para convertir un objeto a su valor primitivo[ref].
Imaginemos que cuando se convierta a string, number o cualquier otro tipo de dato, nuestro password lo regrese ofuscado (no olvides darle al botón verde de play):
c-4 — Otros
Existen otros tipos[ref] (No olvides revisar la documentación), esta vez solo voy a mencionarlos:
- Symbol.hasInstance: Un método que determina si un objeto constructor reconoce al objeto como su instancia y es utilizado por instanceof.
- Symbol.replace: Un método que reemplaza las subcadenas que coinciden con una cadena y es utilizado por String.prototype.replace().
- Symbol.search: Un método que devuelve el índice dentro de una cadena que coincide con la expresión regular y es utilizado por String.prototype.search().
- Symbol.split: Un método que separa una cadena en los índices que coincide una expresión regular y es utilizado por String.prototype.split().
- Symbol.asyncIterator: Crea un bucle iterando tanto sobre objetos iterables y es utilizado por for await…of[ref]
No olvidemos que los símbolos son un tipo de dato único e inmutable y en la parte de metaprogramación son de tipo reflexión.
iv. Generadores
Debo agradecer a mi compañero Alessandro por haberme explicado en su momento este tema.
Antes de ver generadores, repasemos un poco, primero veremos un poco de loops[ref], después continuaremos con iteradores (Symbol.iterator) y finalmente entraremos de lleno a los generadores (*)…
Ejemplo 1.0: Imaginemos que tenemos el siguiente arreglo:
La idea es obtener cada uno de los valores e imprimirlos en consola, quizás lo primero que se te ocurra es hacerlo con un for, forEach, while (básicamente cualquier método Loop[ref]).
Esta genial, y es una muy buena respuesta; ahora, aumentemos un poco la complejidad:
Ejemplo 1.1: Qué pasaría si tuviéramos un objeto de arreglos:
Es una lista de carros dividido por categorías, y la idea obtener cada uno de los modelos de los carros e imprimirlos en consola, quizás, se te ocurra hacer un loop dentro de otro, mandar a llamar a cada uno de manera separada, o usar el operador spread, hay muchas formas de hacerlo, veamos una de ellas:
Qué pasaría si en la lista vienen más modelos de carros, o en vez de sports diga sportsCars, quizás la solución pueda ser:
Se ve mejor ¿no?, veamos el mismo ejemplo, pero usando el iterador Symbol.iterator (no olvides darle al botón verde de play):
Se ve un poco más ehm.. complejo, ¿no?, quizás sí…
Los loops son geniales para recorrer datos, crear un nuevo arreglo, editarlo o hacer cosas sencillas, pero, no tenemos el control al 100% como con los iteradores (Symbol.iterator), con este último, podemos hacer validaciones más complejas, indicarle (mediante returns) si debe pasar al siguiente, reiniciar el loop, o que termine antes, personalmente creo que desarrollar con iteradores puede ser… ehm, como más confuso (al menos al principio, ya después te acostumbras).
Pero… ¿No estaría genial poder combinar estas dos formas?, hacer uso de los loops de manera sencilla y poder generar iteradores simples, rápidos y sencillos, y ¿por qué no? hasta asíncronos; pues si existe… desde la edición de ECMAScript2015 podemos, y es con generadores…
a — Funciones generadoras
La forma de crear generadores es por medio de funciones (generator functions)…
Las funciones generadoras nos permiten un nivel superior de abstracción a iterables, esto quiere decir, que en vez de que generemos un objeto iterable (Symbol.iterator), vamos a crear una función generadora, que va a cumplir con las mismas reglas de un iterador, pero de una manera más fácil y limpia.
Veamos la sintaxis de una función generadora:
Todas estas formas son válidas para crear una función generadora:
Importante: No usar con arrow functions, ya que arroja un error
Veamos un ejemplo sencillo:
Podemos observar que:
- En la línea 7 invocamos la función generadora sumGenerator y le pasamos dos parámetros 10, 5
- En la línea 9 podemos observar que no ejecuta nada de la función, por lo que podemos decir que la función inicia pausada.
- En la línea 10, cuando ejecutamos el método next() (es un método interno de los generadores, que es exactamente igual al que creamos en Symbol.iterator); podemos observar que ejecuta la línea 2, y se pausa en la línea 3 (recordemos que yield lo usamos para pausar un flujo)
- En la línea 11, cuando ejecutamos el método next(), la función generadora continua donde se quedo (en este caso en la línea 3), ejecuta la línea 4, al ser un return lo que va a hacer es retornar la suma de los dos números; podemos observar que la consola imprime {value: 15, done: true}, que es exactamente como funciona en Symbol.iterator.
Entonces:
- Una función generadora SIEMPRE va a iniciar en un estado pausado.
- Al invocar .next() en el generador, en caso de que sea la primera vez, se ejecutará hasta la siguiente palabra clave yield
- Cuando se invoca .next() en el generador, pero no es el primero, se va a ejecutar a partir del último yield, hasta el siguiente que encuentre.
- En caso de que no haya más palabras claves yield o hasta que llegue al return, es cuando termina de invocar la función.
Veamos otro ejemplo:
En consola sale lo siguiente:
Podemos observar que:
- En la línea 11 invocamos la función generadora sumGenerator y le pasamos dos parámetros 10, 5, 8
- En la línea 13 podemos observar que no ejecuta nada de la función, por lo que podemos decir que inicia pausada.
- En la línea 14, cuando ejecutamos el método next(), ejecuta la línea 2, y se pausa en la línea 3 y regresa el valor de x(10)
- En la línea 15, cuando ejecutamos el método next(), se pausa en la línea 4 y regresa el valor de y(5)
- En la línea 16, cuando ejecutamos el método next(), se pausa en la línea 5 y regresa el valor de z(8)
- En la línea 17, cuando ejecutamos el método next(), se pausa en la línea 6 y regresa la suma de x + y + x = (23)
- En la línea 18, cuando ejecutamos el método next(), ejecuta la línea 7, y al no tener más pausas, ni return, termina el flujo {value: undefined, done: true}
Si hemos puesto atención yield tiene otra funcionalidad, que es retornar un valor
Veamos otro ejemplo, ahora iterando un arreglo:
Otra de las ventajas de las funciones generadoras, es la resolución de famoso callback hell de manera síncrona, ya que yield aparte de que pausar la función, mandar un parámetro, también puede recibir un parámetro (de cualquier tipo de dato).
Retomando el ejemplo de sumGenerator mandamos 3 parámetros (x, y, z), pero qué pasaría si en vez de mandar los 3 al mismo tiempo, tuviéramos la posibilidad de mandar uno a uno; veamos el ejemplo:
En consola sale lo siguiente:
Podemos observar que:
- En la línea 15 invocamos la función generadora sumGenerator
- En la línea 17 podemos observar que no ejecuta nada de la función, por lo que podemos decir que inicia pausada.
- En la línea 18, cuando ejecutamos el método next(), ejecuta la línea 2, se pausa en la línea 3, donde va a esperar un valor (x).
- En la línea 19, cuando ejecutamos el método next(10), le estamos mandando un parámetro (10), agrega el valor a la variable (x = 10), se ejecuta la línea 4 y se pausa en la línea 5, donde va a esperar un valor (y).
- En la línea 20, cuando ejecutamos el método next(5), le estamos mandando un parámetro (5), agrega el valor a la variable (y = 10), se ejecuta la línea 6 y se pausa en la línea 7, donde va a esperar un valor (z).
- En la línea 2, cuando ejecutamos el método next(8), le estamos mandando un parámetro (8), agrega el valor a la variable (z = 10), se ejecuta la línea 8.
- La línea 9 hace la suma y lo guarda en la variable sum, se ejecuta la línea 10, y al no haber más yield, regresa el resultado de sum, termina el flujo {value: 23, done: true}
La ventaja de usar yield, es que podemos pausar en cualquier parte de nuestro flujo, mandar datos, y esperar un parámetro; veamos el siguiente ejemplo:
Cuando no estamos acostumbrados a usar generadores, el código de arriba puede ser bastante confuso; voy a intentar ser lo más claro posible, para que puedas comprender qué está pasando, así que vamos línea por línea:
Otra forma de ver el ejemplo:
El ejemplo de arriba, realmente es un caso extremo, que solo nos sirve para ver el poder que tienen los generadores, creo que en un problema real es algo complicado manejarlo así…
Otra de ventaja que nos da la palabra clave yield, es poderlo usar como argumento de una función:
Importante: yield NO SE PASA COMO ARGUMENTO, lo que va a hacer es:
- Hacer una pausa
- Mandar un parametro (si es que lo hay)
- Esperar a recibir otro parámetro, y después invocar la función
Ejemplo: Imaginemos que queremos pedir un uber, la idea es la siguiente:
El código quedaría algo así:
Creo imaginar que estas pensando… ¿Es posible pasar el ejemplo de arriba a llamadas asíncronas (Promise)? La respuesta es SÍ, pero, no es aconsejable hacerlo; veamos un ejemplo con una petición simple:
¿Pueden detectar cuál va a ser nuestro problema?, si no lo han visto, pueden revisar el siguiente ejemplo (con tres peticiones):
Así es!!!, estamos volviendo al callback hell, pero ahora con promesas y generadores!!, y es cuando nos hacemos la segunda pregunta ¿Es posible simplificar el código? La respuesta es SÍ, en ES6 podemos hacerlo, pero necesitamos hacer algunos cambios
En este video (Netflix UI Engineering)[ref], nos da una alternativa; que es básicamente automatizar la llamada de las promesas, esto quiere decir que, en vez de hacer uno a uno nosotros, dejemos que una función se encargue de ir iterando.
Veamos la función:
En el gif, vemos cómo se comporta:
Expliquemos paso a paso:
- A la función spawn debemos de pasarle nuestra función generadora
- Cuando se invoca la función spawn, inmediatamente retorna una promesa (y como sabemos las promesas se ejecutan automáticamente)
- En la línea 73 (de la imagen) invocamos la función onResult (sin parámetros)
- Cuando entra por primera vez en la línea 64 (de la imagen), lo que hace es inicializar nuestro generador
- Cuando nuestra petición ha terminado, nos regresa value (promesa) y done (false), en la línea 65 (de la imagen), validamos si ya terminó nuestro generador y resolvemos, en este caso nos regresa false, entonces…
- En la línea 68 (de la imagen), esperamos a que se resuelva la promesa
- Cuando se resuelve, invoca de nuevo onResult, y le manda los datos que obtuvo de la promesa {movie: {…}}.
- Cuando nuestra petición ha terminado, nos regresa value (promesa) y done (false), en la línea 65 (de la imagen), validamos si ya terminó nuestro generador y resolvemos, en este caso nos regresa false, entonces…
- En la línea 68 (de la imagen), esperamos a que se resuelva la promesa
- Cuando se resuelve, invoca de nuevo onResult, y le manda los datos que obtuvo de la promesa {movie: {…}, video:{…}}
- Cuando nuestra petición ha terminado, nos regresa value (promesa) y done (true), en la línea 65 (de la imagen), validamos si ya terminó nuestro generador y resolvemos, en este caso nos regresa true, entonces…
- En la línea 66 (de la imagen), mandamos la última promesa para que se resuelva
- Esperamos a que se resuelva nuestra promesa, y leemos los datos en la línea 86 (de la imagen)
Veamos todo el código:
Entonces toda nuestra lógica va a ser desarrollada dentro del generador, la ventaja es que podemos ver el código de manera secuencial.
Actualmente existen librerías de terceros como Co.js[ref], que simplifican el trabajo; pero no hay que desanimarse, con la llegada de ECMAScript2016 (ES7), se agregó lo que es async/await (que ya veremos más adelante**) y con esos operadores es más sencillo y simple…
Usar generadores no siempre es lo mejor, ya que como todo, tiene algunas desventajas:
- Sobrecarga de rendimiento, muchas veces es más rápido y mejor usar promesas o callbacks.
- No se puede usar recursividad de funciones.
- La palabra clave yield no puede ser usada dentro de otras funciones.
- Si hacemos peticiones asíncronas, volvemos al problema del callback hell, por lo que debemos de recurrir a código extra o librería de terceros.
En la siguiente entrega vamos a ver JavaScript — ES6 (Parte IV)
La entrega pasada vimos JavaScript — ES6 (Parte II)
Bibliografía y links que te puede interesar…