ES6: el remozado JavaScript. Parte III: clases y otras novedades del lenguaje

2
2433

Índice de contenidos

[teaser img=”https://www.adictosaltrabajo.com/wp-content/uploads/2018/04/es6_parte3.png”]
El objetivo de este artículo es introducir las novedades de ES6 relativas a clases y a otras funciones nuevas del lenguaje.
[/teaser]

[alert type=”warning” close=”false”]Este post es la continuación de: ES6: el remozado JavaScript. Parte II: funciones, objetos y arrays[/alert]

6. Clases

En JavaScript clásico hay dos corrientes principales para tratar con objetos:

  • una definir directamente el objeto sin preocuparnos de cuál es su clase, y si ésta nos es necesaria, preguntar por su prototipo y modificarlo a conveniencia.
  • Definir una función que será el constructor del ejemplo.

Si vamos a tratar con un objeto, que tendrá una única instancia, se define el objeto directamente:

Pero cuando podemos tener varias instancias será necesario definir una forma de crear objetos. Hasta ahora se hacía mediante una función constructora con la palabra reservada new.

Pero imaginemos que en lugar de dos instancias del objeto “pelota” hay miles de instancias, todas moviéndose, y rebotando por la pantallita.

Cada método que hemos añadido a la clase Pelota, tiene una dirección de memoria distinta apuntándole a él. En otros lenguajes como Java ésto es normal, pero en los navegadores históricamente se ha controlado mucho la memoria, ya que la aplicación se ejecuta en el navegador, que hasta no hace mucho, tenía unos recursos muy limitados.

Entonces, ¿es necesario que haya miles de funciones iguales que renderizan la posición de la pelota? ¿o sería suficiente con definir una única función externa?

Lo único malo de hacerlo así es que la función parece que no tiene nada que ver con el objeto. Lo que se hacía era obtener el prototipo del objeto, y añadirle la función en cuestión.

Y con los prototipos llega uno de los mayores fracasos de JavaScript en cuanto a su popularidad. Y es que ha fracasado totalmente a la hora de comunicar a programadores de otros lenguajes de programación su funcionamiento. Durante años, los programadores de Java han intentado programar JavaScript como si fuera un lenguaje de Programación Orientado a Objetos, como el que ellos conocen, como Java. Y no. JavaScript es un lenguaje orientado a objetos sin clases. En su lugar todo objeto tiene un prototipo, que no es sino otro objeto.

6.1. Entendiendo los prototipos

El prototipo de un objeto es un objeto que nos devuelve su constructor y sus métodos. Con lo cual se puede recrear el objeto a partir de su prototipo.

Si preguntamos por el prototipo de la clase Rectángulo nos devuelve su constructor y el método area.

Y podemos definir la clase Cuadrado en base al prototipo de Rectángulo:

Aunque no hayamos definido el área del cuadrado, está definida por la herencia del rectángulo:

Y vemos que cuadrado1 es una instancia de Cuadrado, pero también es una instancia de Rectángulo. En este caso se dice que está en la cadena de prototipos.

Y ésto es uno de los mayores motivos de desasosiego para los programadores de otros lenguajes, que esperan que JavaScript se comporte como ellos esperan siguiendo el paradigma de POO que conocen. Es por eso, que la comunidad de desarrolladores, en lugar de aprender el lenguaje, ha ejercido presión para que éste evolucionara por unos caminos donde no les fallara la intuición. Y por eso, en ES6 se incluyen clases y modularización, que per sé no es malo, y así se acerca a una comunidad mucho más amplia.

6.2. Clases

Esta necesidad se ha visto satisfecha en la versión ES6. Y así, ahora tenemos clases para definir objetos.

Realmente JavaScript no ha cambiado su paradigma orientado a prototipos. Esta forma de escribir clases es meramente azúcar sintáctico que por debajo se traduce en lo visto en el punto anterior, pero al menos, es una notación con la que la mayoría de desarrolladores están más familiarizados.

Y nos permite definir una herencia de una forma mucho más amigable.

6.3. Módulos

En ES6 se introduce el concepto de módulo que no es otro que permitir trocear el código en distintos ficheros, de forma que se permita la exportación de clases, funciones, variables o constantes e importarlos desde otro módulo.

Así, nuestro ejemplo anterior quedaría guardado en un fichero poligonos.js con el siguiente código:

O si preferimos exportar y separar la definición de las clases, de cuáles exportamos:

Y para importarlas desde otro fichero y hacer uso de ellas:

Y esto se puede hacer tanto a nivel de clase, como de funciones, variables o constantes. Todo muy útil, y muy esperado por una comunidad de desarrolladores acostumbrados a modularizar su código. Las ventajas son incontestables, empezando desde el principio de responsabilidad única, a una simple organización de código. Pero aquí es donde viene la mala noticia, y es que aunque está definido en el estándar, no se ponen de acuerdo cómo implementarlo en los navegadores. Y ninguno de ellos lo implementa de forma nativa.

De todas formas, vamos a meditar qué significa implementar ésto en el navegador: la realidad es que la página html importaría un fichero JavaScript, que a su vez importaría a otros ficheros JavaScripts resolviendo todas sus dependencias e importando para ello más ficheros JavaScript. La resolución en cascada podría traducirse en miles de llamadas al servidor para traerse pequeños módulos JavaScript. Y aquí es donde viene uno de los cambios que han revolucionado el mundo front, y es que antes para ejecutar JavaScript te valía un navegador, y ahora se requiere un proceso de “compilación”, que normalmente realiza un TaskRunner tipo grunt, gulp, parcel o webpack., que empaqueta todo nuestro código diseminado por módulos en un único fichero (bundle) entendible por el navegador, que a veces ha sufrido procesos de minificación, de validación, transpilación a ES5 o varias cosas más.

En cualquier caso, hay un pollyfill para cargar módulos de forma estándar.

7. Otras novedades en el lenguaje

Son muchas las novedades que se incluyen en esta nueva especificación, y que vienen heredadas de lo que pasa en otros lenguajes.

7.1. Promesas

A medida que las interfaces son más atractivas, requerimos que la interacción con las mismas sea más inmediata, consiguiendo una buena experiencia de usuario, que de otro modo sería pobre. Pero al contrario de lo que ocurre en otros lenguajes, donde se pueden ejecutar varios hilos de ejecución concurrentemente, JavaScript tiene un único hilo de ejecución de forma que hasta que no ejecuta una instrucción, no pasa a la siguiente. Eso puede llegar a producir retrasos indeseables, e incluso pérdida de control por parte del usuario.

Hay que entender el hilo de ejecución como una cola. Cuando vinculamos una acción a un evento, lo que estamos haciendo es encolar la acción al final de la cola de instrucciones a ejecutar, y hasta que no se termine, no devolverá el control. Por ejemplo, definimos que al pulsar una tecla se ejecute una acción. Ésta no es inmediata y se encola.

A veces nos interesa cierta asincronía adrede, cuando queremos que se ejecute cierta instrucción pero nos interesa devolver el control inmediatamente, para no dejar bloqueada la pantalla.

El código anterior ejecuta la instruccion1, encola la instruccion2 para que se ejecute cuando pueda, y devuelve el control a la línea siguiente.

Este tipo de llamadas se llaman callbacks y se ejecutan cuando sea, pero la instruccion1 ya ha devuelto el control. Introduce la asincronía, y es la forma clásica en las versiones anteriores de JavaScript.

Pero tiene un problema bastante engorroso de lidiar con él, y es que los callbacks se pueden anidar con un nivel de complejidad que es el llamado Infierno de Callbacks.

Una promesa es una nueva forma de implementar esta asincronía pero sin usar callbacks, lo que hace que se lea mucho más fácil.

El ciclo de vida de una promesa es muy sencillo: cuando se crea se queda en estado pending esperando a que se resuelva o se rechace.

Y podríamos invocar así:

Una promesa tiene 3 posibles estados: pendiente, resuelta y rechazada. Cuando la invocamos queda en estado pendiente, y se inicia la asincronía, y cuando se obtiene un resultado, se invoca con éste a resolve() o si se produce un error a reject(). El caso es que desde fuera no podemos consultar el estado de una promesa, pero podemos saber cuando cambia su estado mediante el then().

Y el resultado de ejecutar el código anterior es:

Lo que ha pasado es lo siguiente: lo primero que se ha lanzado es la función que se le pasa a la promesa, y pinta “Promesa pendiente”. Luego se devuelve el control a la línea de ejecución y pasa a la siguiente instrucción y escribe en la consola “Hola Mundo!”, y acto seguido se resuelve la promesa, en nuestro caso cuando invocamos a resolve(), y entonces se lanza lo que hay en el then() y escribe “Promesa resuelta” en la consola.

El argumento que se le pasa a la promesa se llama función ejecutora. Cualquier error que se produzca en un bloque try … catch se resuelve la promesa lanzando el método reject().

Cuando se rechaza una promesa se puede ejecutar de dos formas: como la segunda función de un then() o como un catch(). El siguiente código sería equivalente:

Y escribiría por consola: “Error: Se rechaza sin llamar al siguiente resolve”.

Se pueden tener varias promesas, y querer que se lance algo cuando se cumplan todas o que se rechace con que una falle.

7.1.1. Async y await

Esta funcionalidad aún no está disponible en ES6, aunque muchos transpiladores ya permiten que la usemos. Se espera que esté en la próxima liberación del estándar ES7, o ES2017.

Básicamente es lo mismo que las promesas, para introducir asincronía, pero con una sintaxis ligeramente distinta y a mi parecer más sencilla. Comienza con preceder una función de la palabra async para indicar que en esa función hay alguna parte que es asíncrona. Y luego, en la parte que debe esperar a resolver una promesa, se precede de la palabra reservada await indicando que debe esperar hasta que ésta queda resuelta.

7.2. Proxies e Interceptores

Esta nueva versión de JavaScript nos aporta una funcionalidad existente en otros lenguajes y que se echaba en falta y es la posibilidad de definir Proxies e Interceptores sobre objetos. Como no se trata de azúcar sintáctico, sino que es algo totalmente nuevo de ES6, es una característica que no puede ser emulada por un polyfill, ni transpilada a ES5.

En esencia un Proxy es un objeto que recubre a otro ya existente, y que permite interceptar algunas operaciones sobre él. Veámoslo con un ejemplo:

¿Qué pasa cuando consultamos el valor del lado del nuevo objeto?

¿Y cómo es que no ha devuelto 4? Sólo ha interceptado el get, y no hemos devuelto la respuesta de lo que vale la propiedad cuadrado.lado

Ahora sí. Si repetimos la prueba obtendremos el valor del lado. ¿Y si consultamos el valor del área, que no es una propiedad sino un método?

Cuando consultamos el área nos retorna el valor correcto, sin embargo vemos que se han escrito por consola tres consultas a propiedades: una para el área, y como para calcular el área hay que hacer lado * lado, se accede dos veces más a la propiedad lado. De ahí que salga tres veces.

Aquí estamos usando el interceptor de forma inocente para mostrar un mensaje por consola, pero se puede utilizar para alterar el resultado de métodos y operaciones, para formatear campos numéricos, fechas, etc… o incluso para internacionalizar mensajes de error.

A todos los efectos es como si fuese el mismo objeto, que cuando accedemos a través de su versión proxy intercepta la consulta de sus propiedades. Pero centrémonos en esa afirmación: “es como si fuese el mismo objeto”. No es exactamente cierta, pero se parece mucho. Si cambiamos la propiedad en el objeto proxy, cambia en el objeto al que se refiere, y viceversa.

Se puede utilizar para hacer validaciones al momento de asignar un valor a una propiedad. En el ejemplo que nos ocupa, se podría validar que el lado fuera siempre un número entero positivo:

Hay muchos métodos de los objetos que se pueden interceptar. La documentación del objeto Proxy y sus handler es muy exhaustiva.

7.3. Strings y Literales

Otra de las novedades que se echaban en falta era poder definir cadenas que ocuparan varias líneas. Los templates strings vienen a solventar ese problema sin necesidad de reemplazar /n por <br> ni similares. Sólo hay que encerrar el texto entre comillas invertidas.

También sirven para interpolar resultados:

Aquí se mostrará por consola El precio es 3569.5 euros. Hay que tener en cuenta, que estas expresiones se evalúan sólo cuando se definen, no cuando se consultan.

Si asignamos a precio 10 y volvemos a mostrar por consola la cadena nos seguirá mostrando el mismo mensaje anterior de El precio es 3569.5 euros. No cambia aunque ahora el precio sea 10.

Si queremos hacer que se evalúen cada vez se pueden usar las plantillas de texto con postprocesado (tagged template literals), que no son otra cosa que definir una función con la plantilla que se quiere que se devuelva, y por otro llamar a esa función pasándole los parámetros con los que debe interpretarse. En el ejemplo que tenemos entre manos:

Con los template literals los caracteres como retornos de carro /n y tabuladores /t se convierten en retornos de carro y tabuladores de verdad.

Pero a veces, nos interesa evaluar una interpolación, pero preservar estos caracteres. Existe un nuevo método dentro del objeto String que nos permite preservar el texto tal y como se escribió, pero evaluando las interpolaciones: String.raw.

Y por consola saldrá Gracias por su visita.\n\tEl precio es:\n\t3569.5 euros.

7.3.1. Nuevos métodos de String

Se añaden nuevos métodos, que si bien se podían hacer antes con el indexof de ES5, facilitan la vida al programador:

  • startsWith: comprueba si una cadena comienza por otra
  • endsWith: comprueba si una cadena finaliza por otra
  • Includes: comprueba si una cadena está incluida en otra
  • repeat: repite una cadena n veces

7.4. Novedades en los objetos Math y Number

La primera novedad que incluye ES6 es la posibilidad de definir enteros por su forma octal o binaria. Hasta ahora se podía hacer en hexadecimal, usando el prefijo 0x para indicar que lo que seguía era una representación hexadecimal. Ahora se puede usar el prefijo 0b para indicar que lo que sigue es la representación binaria de un número y 0o para indicar que lo que sigue es la representación octal de un número.

Se introducen muchos métodos en Number, la mayoría ya viejos conocidos. Funciones existentes, que ahora se engloban bajo el objeto Number para darle una perspectiva más propia de un paradigma orientado a objetos. Así la función isNaN() se puede usar como Number.isNaN(), aunque tienen algún matiz. Lo mismo con isFinite(), que ahora se puede usar como Number.isFinite().

Se introduce Number.isInteger(value) y Number.isSafeInteger(value).

Pero para mí, una de las novedades introducidas más importantes, es Number.EPSILON. Una constante, que nos sirve para comparar con el error de imprecisión por la operativa de coma flotante. La descripción exacta de este valor es: la distancia mínima entre 1 y el siguiente número representado en como flotante, que es 2,22e-16. Una unidad muy pequeña. No confundir con el número e.

Y de alguna forma nos viene a recordar el problema de comparar números en JavaScript. Ya vimos que 0.1 + 0.2 no es 0.3 y sin embargo 0.25 + 0.125 sí es 0.375. Vamos a profundizar en el problema.

La representación de 0.1 y 0.2 en coma flotante es eso, una representación. Una aproximación al número real más cercano que se puede representar. En este caso los decimales más cercanos con representación son:

Vemos que la distancia entre la representación del resultado de sumar 0.1 + 0.2 y la representación de 0.3 es del orden de 5.55 x 1017

Sin embargo, ¿por qué no pasa con 0.25 y 0.125? Pues porque tienen representación binaria exacta al poderse representar su denominador como potencias de 2. Uno es 22 y el otro 23

Eso quiere decir que siempre que hagamos aritmética en coma flotante, ya sea en JavaScript, como en cualquier otro lenguaje que use este estándar para representar números, debemos tener en cuenta la precisión.

7.4.1 La clase Math

Incluye numerosas constantes y funciones matemáticas entre las que en esta versión se han añadido todas las razones trigonométricas hiperbólicas:

Nuevas funciones para manejar logaritmos que no estén en base e:

  • Math.log10(): logaritmo en base 10
  • Math.log2(): logaritmo en base 2

Mención aparte merecen

  • Math.log1p(x) es el logaritmo neperiano de x+1. Se suele usar para valores muy pequeños que tienen problemas de precisión.
  • Math.expm1() es ex -1

Y otras funciones son:

  • Math.hypot(x,y): es la hipotenusa. Es decir la raiz cuadrada de la suma del cuadrado de sus catetos.

ya que 32 + 42 = 52

Pero además, es el módulo del vector libre (3,4). De hecho, podemos usar esta función para obtener el módulo de un vector de n-dimensiones, ya que admite n argumentos.

El resultado es la raíz cuadrada de (a12 + a22 + … + an2)

  • Math.trunc(x): devuelve la parte entera del número x
  • Math.sign(x): devuelve si un número es positivo (1), negativo (-1) o cero (0).
  • Math.imul(x,y): devuelve el resultado de una multiplicación de 32 bits
  • Math.fround(x): devuelve la representación en coma flotante más cercana del número x
  • Math.cbrt(x): devuelve la raíz cúbica de x
  • Math.clz32(x): devuelve el número de ceros por la izquierda en la representación binaria del número x con 32 bits. Aún no se me ocurre en qué casos puede ser útil este método.

8. Epílogo

Durante muchos años, JavaScript se ha mantenido estable y sin evolucionar, pero el redescubrimiento de sus posibilidades por parte de los desarrolladores ha empujado una evolución, que lo ha rejuvenecido ampliando sus posibilidades. Sin duda es un lenguaje que está vivo y que a día de hoy es cambiante.

Recomiendo emplear el estándar, que para eso está. Y antes de profundizar en otros frameworks que se apoyan en ES6, conocer de primera mano la tecnología que subyace a estos frameworks.

Muchos desarrolladores se refieren despectivamente a desarrollar con JavaScript a pelo como “Vainilla JS” en referencia a la carestía del lenguaje. Siento discrepar, y no puedo evitar confesar que amo profundamente este lenguaje, y su versatilidad con el tipado dinámico. Mientras pueda, defenderé el uso del estándar frente a frameworks que se pueden quedar obsoletos o desmantenidos en cualquier momento.

9. Referencias

2 Comentarios

  1. Citándote
    “Siento discrepar, y no puedo evitar confesar que amo profundamente este lenguaje, y su versatilidad con el tipado dinámico. Mientras pueda, defenderé el uso del estándar frente a frameworks que se pueden quedar obsoletos o desmantenidos en cualquier momento.”

    Mis APLAUSOS jaja.

    Algunos prefieren sufrir cuando…en su Hosting sucede…
    Ohhh actualizaron la versión de Javascript
    Ohhh actualizaron la versión de PHP
    Ohhh noooo cambiaron la version del WordPress

    Señores y señoras, aprendan a pensar con arquitectura OOP y Javascript Puro y se les terminarán los problemas.
    Eventualmente sólo deberán cambiar una o dos líneas y su APP o WEB seguirá funcionando y NO HABRA DOLORES DE CABEZA.
    Mis Saludos

Dejar respuesta

Please enter your comment!
Please enter your name here