DEV Community

Cristian Fernando
Cristian Fernando

Posted on

Apología de un lenguaje ambiguo: El meme definitivo para entender (o no) JavaScript avanzado 😎

Hace poco encontré en internet un meme de javascript que me pareció super interesante. Por este motivo intentaré explicar en este post por qué javascript puede llegar a ser tan raro.

El meme per se es:

img


Índice

  1. typeof NaN es number
  2. "Problemas" de redondeo en enteros y decimales
  3. Math.max(), Math.min() y el problema de los Infinity
  4. ¿Sumas o concatenaciones? Arreglos y objetos
  5. Operaciones con booleanos
  6. Que carajos es (! + [] + [] + ![]).length
  7. Sumas y concatenaciones de enteros
  8. Truthy vs Falsy
  9. Conclusiones
  10. Referencias

1. typeof NaN es number

El operador unario typeof regresa el tipo de dato de la variable a la que se lo aplique.
La siguiente tabla resume todos los posibles casos con los que nos podemos encontrar:

Valor Resultado
Variables no declaradas "undefined"
undefined "undefined"
null "object"
Booleans "boolean"
Numbers "number"
Strings "string"
Functions "function"
Symbols "symbol"
Cualquier otro valor "object"

Ahora bien, porque cuando intentamos obtener el tipo de un NaN el resultado es number:

console.log(typeof NaN); // number
Enter fullscreen mode Exit fullscreen mode

Encontré algunas respuestas en foros que me resultaron algo ambiguas, tratare de explicarlo con palabras simples:

Primero, ¿Qué es, o mejor dicho que hace que un valor sea considerado NaN?

En muchas ocasiones te debiste haber encontrado con un NaN al hacer alguna operación en una calculadora científica, entonces, NaN no es un valor propio de javascript, este concepto va más lejos del lenguaje de programación como tal, puede ser considerado y definido de manera técnica desde un punto de vista netamente matemático, pero justamente para no caer en tecnicismos y continuar con la simplicidad que pretendo podemos quedarnos con que javascript nos arrojará un NaN en los siguientes casos:

  • Cualquier división entre 0.
  • División de infinito entre infinito.
  • Multiplicación de infinito por 0.
  • Cualquier operación que tenga a NaN como operando.
  • Conversión de un string no numérico o un undefined a number.
  • Cualquier valor numérico que no esté incluido en el intervalo de números soportado por el lenguaje.

Acá algunos ejemplos para ilustrar mejor lo antes dicho:

console.log(typeof 5/0); //NaN
console.log(typeof Infinity / Infinity); //NaN
console.log(typeof Infinity * 0); //NaN
console.log(typeof [] - NaN); //NaN
console.log(Number("hola")); //NaN
console.log(Number(undefined)); //NaN
console.log((3.2317006071311 * 10e616) / (3.2317006071311 * 10e616)); // NaN
Enter fullscreen mode Exit fullscreen mode

Con todo esto solo sabemos cuándo en javascript es un valor NaN, ahora veremos por qué NaN es de tipo number.

La respuesta es más sencilla de lo que parece, el estándar ECMAScript, quienes dan mantenimiento al lenguaje, estableció que para cualquier valor numérico los datos deben adaptarse al estándar IEEE-754, esto a groso modo indica que los números en javascript deben ser de punto flotante, deben incluir Infinity y -Infinity y (ohh sorpresa) también el valor NaN.

Si vemos un poco más de cerca el conjunto ejemplos de la parte superior, podemos darnos cuenta que un NaN aparece cuando intentamos realizar algún tipo de operación con números, este es el común denominador que comparten todos los ejemplos, de alguna u otra manera al manipular números bien sean como simples valores primitivos, usando Infinity, -Infinity o el propio NaN (ahora sabemos que estos 3 por el estándar IEEE-754 se relacionan de manera directa con los números del lenguaje) es donde NaN se origina. Esto tiene todo el sentido del mundo.

En cuanto al caso de:

console.log((3.2317006071311 * 10e616) / (3.2317006071311 * 10e616)); // NaN
Enter fullscreen mode Exit fullscreen mode

En aritmética tradicional el resultado esperado de esta operación sería 1 porque tanto el numerador como el denominador de la operación es el mismo.

Es bastante peculiar ya que a simple vista es una operación que se debería resolver ¿verdad?

El problema acá radica en que javascript solo soporta números dentro de un intervalo específico, si algún dato sale de este intervalo entonces el intérprete se queja arrojando un NaN.

La aritmética informática no puede operar directamente con números reales, sino solo con un subconjunto finito de números racionales, limitado por la cantidad de bits utilizados para almacenarlos.

Para saber el máximo y el mínimo de valores que javascript puede aceptar podemos hacer lo siguiente:

console.log(Number.MIN_VALUE); // 5e-324
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
Enter fullscreen mode Exit fullscreen mode

Como los valores del ejemplo exceden estos límites, javascript dice algo como: hey! estos números que quieres que divida son extremadamente grandes para mi, así que los redondeará a Infinity, pero Infinity / Infinity da NaN, entonces te muestro un NaN. Gracias por usar JavaScript!

En conclusión, por el estándar IEEE-754 los valores NaN, Infinity y -Infinity están ligados directamente a los números en javascript; por este motivo al intentar obtener el tipo de dato de cualquiera de estos valores obtendremos number.

console.log(typeof NaN); //number
console.log(typeof Infinity); //number
console.log(typeof -Infinity); //number
Enter fullscreen mode Exit fullscreen mode

Espero que haya quedado claro.

2. "Problemas" de redondeo en enteros y decimales

¿Por qué 9999999999999999 (son 16 9's) redondea a 10000000000000000?

¿Por que 0.1 + 0.2 == 0.3 nos da false?

¿Por que 0.5 + 0.1 == 0.6 nos da true?

Nuevamente al intentar hacer operaciones que para un ser humano parecen lógicas, Javascript termina sorprendiendo y nos arroja resultados que nos provocan más de un dolor de cabeza.

Para comprender el por que de estas preguntas debemos remitirnos una vez más al estándar IEEE-754 que usa el lenguaje para la manipulación de valores numéricos.

Según este estándar, javascript almacena los números en un formato de 64 bits también llamado formato de doble presión:

  • El primer bit se reserva para el signo del número
  • 11 bits almacenan la posición del punto flotante.
  • 52 bits el número como tal.

Dando un total de 64 bits por cada número guardado en memoria.

El número 9999999999999999 cuando lo convertimos a su forma binaria e intentamos almacenarlo en 64 bits se desborda, es muy grande para guardarlo con exactitud, entonces javascript nuevamente en vez de lanzarnos un error hace internamente un proceso de redondeo que no vemos e intenta lanzarnos un resultado más o menos lógico. Obviamente no es el resultado que esperábamos.

Con el caso de los decimales pasa algo similar.

En el ejemplo:

console.log(0.1 + 0.2 == 0.3); // false
Enter fullscreen mode Exit fullscreen mode

Solo las fracciones con un denominador que es una potencia de dos pueden representarse finitamente en forma binaria. Dado que los denominadores de 0,1 (1/10) y 0,2 (1/5) no son potencias de dos, estos números no se pueden representar de forma finita en un formato binario.
Entonces Javascript nuevamente tendrá que realizar un proceso de redondeo implícito para intentar arrojar un resultado más o menos lógico, es en este proceso de redondeo que se pierde presión.

En el ejemplo:

console.log(0.5 + 0.1 == 0.6); // true
Enter fullscreen mode Exit fullscreen mode

0.5 en fracción es 1/2 el denominador si es potencia de 2 entonces al el número 0.5 puede ser almacenado de manera precisa en formato binario en memoria.

El algoritmo que usa IEEE-754 para convertir los números en javascript a formato binario de 64 bits es bastante complejo de explicar en pocas palabras, dejare links en las referencias de este post por si deseas saber mas el respecto.

En conclusión, estas operaciones raras en javascript se dan por el uso y aplicación del estándar IEEE-754. Puede resultar muy confuso para el ser humano, pero las computadoras lo entienden muy bien. Por motivos como estos muchos desarrolladores experimentados recomiendan tratar siempre de usar números enteros en los programas y evitar siempre que sea posible las operaciones con números decimales.

3. Math.max(), Math.min() y el problema de los Infinity

Math.max() y Math.min() son 2 maneras de encontrar el máximo y el mínimo de una lista de números. Es muy sencillo de comprender.

¿Cómo podemos saber si un número es mayor o menor a otro? No hace falta más que compararlos.

Si tenemos algo como esto:

console.log(Math.max(5)); //5
Enter fullscreen mode Exit fullscreen mode

El resultado será a fuerza 5 ya que como no hay contra qué comparar, entonces devolvemos el único número.

Entonces qué pasa si hacemos esto:

console.log(Math.max()); // -Infinity
Enter fullscreen mode Exit fullscreen mode

La respuesta textual la hallamos en la MDN:

-Infinity es el comparador inicial porque casi todos los demás valores son mayores, por eso cuando no se dan argumentos, se devuelve -Infinity.

Como no tenemos nada que comparar, JavaScript toma el valor más pequeño posible como valor por defecto o comparador inicial.

Entonces, Math.max() comienza con un valor de búsqueda de -Infinity, porque cualquier otro número será mayor que -Infinity.

console.log(Math.max()); // el dev ve esto
console.log(Math.max(-Infinity)); // javascript ve esto
Enter fullscreen mode Exit fullscreen mode

Esta misma lógica sirve para:

console.log(Math.min()); // Infinity
Enter fullscreen mode Exit fullscreen mode

Como no hay ningún valor para comparar javascript usa el valor más grande posible que es Infinity como comparador inicial o valor por defecto.

console.log(Math.min()); // el dev ve esto
console.log(Math.min(Infinity)); // javascript ve esto
Enter fullscreen mode Exit fullscreen mode

4. ¿Sumas o concatenaciones? Arreglos y objetos

Aplicar el operador + entre arreglos y objetos es de lo más confuso que puede existir en javascript.
Para poder entender el porqué de los resultados extraños es menester entender primero como javascript convierte objetos primitivos.

Pasos para la conversión de un objeto a un primitivo:

  1. Si la entrada es un primitivo, entonces devolvemos el mismo valor.
  2. De lo contrario la entrada es un objeto, entonces aplicamos el método valueOf. Si el resultado es primitivo, lo devolvemos.
  3. De lo contrario llamamos al método toString. Si el resultado es primitivo, lo devolvemos.
  4. De lo contrario, devolvemos un TypeError.

Vamos los ejemplos del meme:

[] + []

Cuando intentamos realizar esta operación, el resultado es una cadena vacía "".
Apliquemos los pasos de conversión de objeto a primitivo (recuerda que los arreglos en javascript son considerados de tipo objeto):

  • Ninguna de las entradas es un primitivo.
  • Aplicamos el método valueOf:
console.log([].valueOf()); // []
Enter fullscreen mode Exit fullscreen mode

Aún seguimos obteniendo el mismo arreglo vacío.

  • Aplicamos el método toString:
console.log([].toString()); // ""
Enter fullscreen mode Exit fullscreen mode

Ahora obtenemos un arreglo vacío, entonces al intentar realizar [] + [] es como si intentáramos concatenar dos cadenas vacías "" + "" que nos dará como resultado otra cadena vacía.

console.log([].toString() + [].toString()); //""
Enter fullscreen mode Exit fullscreen mode

[] + {}

Ahora intentamos concatenar un arreglo con un objeto, ambos vacíos.

Ya sabemos que el arreglo vacío convertido a primitivo es una cadena vacía, entonces intentemos aplicar los pasos de conversión al objeto.

  • El objeto no es un primitivo.
  • Aplicamos el método valueOf:
console.log({}.valueOf()); // {}
Enter fullscreen mode Exit fullscreen mode

Obtenemos el mismo objeto vacío.

  • Aplicamos el método toString:
console.log({}.toString()); // "[object Object]"
Enter fullscreen mode Exit fullscreen mode

Al convertir un objeto a primitivo obtenemos "[object Object]"

La operación entonces se vería así:

console.log("" + "[object Object]"); // "[object Object]"
Enter fullscreen mode Exit fullscreen mode

{} + []

Ahora intentamos realizar la concatenación de un objeto con un arreglo, ambos vacíos.

Lo lógico acá es pensar en la clásica propiedad asociativa de la adición, ¿si [] + {} es "[object Object]" entonces {} + [] debería ser igual no? Lastimosamente no es así.

En este caso el objeto es el primer operando de la operación, pero javascript no lo toma como objeto, sino como un bloque de código vacío:

{

}

+[]
Enter fullscreen mode Exit fullscreen mode

Entonces cómo ejecutamos el código de arriba a abajo, el intérprete entra y sale del bloque vacío, nos queda +[].
Por sino lo sabias el operador + es un shorthand de Number, entonces podemos convertir a number usando este operador.

Ya sabemos que [] es igual a una cadena vacía, y una cadena vacía es un valor falsy, por ello al convertirlo a number tendremos 0.

Este comportamiento es posible cambiarlo si agrupamos la operación para formar una expresión, de la siguiente manera:

({} + []) // "[object Object]"
Enter fullscreen mode Exit fullscreen mode

o si usamos variables para realizar la operación:

const obj = {};
const arr = [];

console.log(obj + arr); // "[object Object]"
Enter fullscreen mode Exit fullscreen mode

{} + {}

Muy similar al último ejemplo, pero el resultado de concatenar 2 objetos vacíos dependerá de en qué navegador lo ejecutes.

En Mozilla:
Nuevamente el primer operador no será evaluado como objeto sino como bloque vacío de código. Entonces nos queda +{}, el resultado de convertir un objeto a número es NaN.

En Chrome:
Evalúa toda la operación como una expresión, ya sabemos que un objeto vacío convertido a primitivo es "[object Object]", solo tendríamos que concatenarlo y el resultado serie "[object Object][object Object]".

5. Operaciones con booleanos

true + true + true === 3

Por aserciones de tipos true se convierte en 1.
Entonces tendríamos 1 + 1 + 1 === 3.
El operador === compara tanto el valor como el tipo de dato, 3 === 3 daria true.

true - true

Nuevamente por aserción de tipos, true adopta el valor de 1.
Entonces tendríamos 1 - 1 dando como resultado 0.

true == 1

El operador == solo compara el valor, tendríamos 1 == 1, el valor es el mismo, entonces el resultado sería true.

true === 1

El operador === compara tanto el valor como el tipo de dato. Entonces un operando es boolean y el otro number, por ello el resultado sería false.

6. Que carajos es (! + [] + [] + ![]).length

Si ver este ejercicio ya resulta un poco intimidante, el resultado es más risible que sorprendente. Aunque no lo creas la respuesta para esta operación es 9.

Para comprender bien cómo se llega a esa respuesta, debemos parcelar el ejercicio:

//En vez del ejercicio original:
console.log((! +[] + [] + ![]).length)

//Podemos escribirlo de la siguiente manera:
console.log(( (! + []) + [] + (![]) ).length)

Enter fullscreen mode Exit fullscreen mode

Aquí diferenciamos 3 operaciones separadas bien definidas:

  • ! + [] Ya sabemos que un arreglo convertido a primitivo da una cadena vacía "", el operador + convertirá la cadena vacía a tipo number, como una cadena vacía es un valor falsy la conversión nos dará 0; finalmente negamos el cero !0, cero también es un valor falsy por ende negado sería true. Visto en código sería algo así:
console.log(! + []); // true
console.log(! + ""); // true
console.log(! + 0); // true
console.log(!0); //true
console.log(!false); //true
console.log(true); //true
Enter fullscreen mode Exit fullscreen mode
  • []
    El segundo operando es solo un arreglo vacío, esto llevado a primitivo es "".

  • ![]
    Finalmente, un arreglo vacío negado. El arreglo vacío es un valor truthy y cómo va acompañado del operador de negación el resultado sería false.

Después de todas estas operaciones intermedias el ejercicio tendrá la siguiente pinta:

console.log(( true + "" + false).length);
Enter fullscreen mode Exit fullscreen mode

Esto es mucho más fácil de operar:

  • true + "" Al concatenar true con la cadena vacía, el valor booleano se transforma a string:
console.log(( "true" + false).length);
Enter fullscreen mode Exit fullscreen mode
  • "true" + false Otra vez una concatenación de cadena con booleano:
console.log(( "truefalse").length);
Enter fullscreen mode Exit fullscreen mode

Para finalizar aplicamos length a la cadena dando como resultado el 9 que tanto nos extrañaba al comienzo.

7. Sumas y concatenaciones de enteros

9 + "1"

Se intenta sumar un string con un number, como no es una operación válida javascript intenta hacer su mejor esfuerzo para darnos un resultado lógico, entonces hace lo que que se denomina una aserción de tipo y tras bambalinas convierte al 9 en string. La suma se vuelve ahora una concatenación de cadenas que da como resultado "91".

console.log(9 + "1"); // El dev ve esto
console.log("9" + "1"); // JavaScript lo interpreta así
Enter fullscreen mode Exit fullscreen mode

9 - "1"

El operador + sirve para muchas cosas en javascript, desde sumar números, concatenar cadenas hasta convertir valor a tipo number.
El operador - es más simple, solo se usa para restar números, por ello es que nuevamente acá el lenguaje hace una aserción de tipos pero esta vez convierte el string "1" a tipo number dando como resultado 91.

console.log(91 - "1"); // El dev ve esto
console.log(91 - 1); // JavaScript lo interpreta así
Enter fullscreen mode Exit fullscreen mode

8. Truthy vs Falsy

Los valores truthy y falsy son básicos en javascript, te dejo unos links en las referencias del post para que puedas saber más de ellos.

[] == 0

Ya sabemos que [] convertido a primitivo es "".
Tanto "" como 0 son valores falsy.
Usamos el operador == u operador de comparación débil entonces solo comparamos los valores mas no los tipos de datos.

console.log([] == 0); // El dev ve esto
console.log(false == false); // JavaScript lo interpreta así
Enter fullscreen mode Exit fullscreen mode

9. Conclusión

JavaScript puede ser un lenguaje bastante bizarro y por ello mismo muchos devs sobre todo de la vieja escuela pueden tirarle mucho hate, pero cuando uno comprende el porqué de las cosas, cuando vamos a la esencia del lenguaje e intentamos entender por qué las cosas pasan de una determinada manera y no de otra es cuando recién nos damos cuenta los motivos de los hechos.

Espero que el post te haya gustado y sobre todo ayudado en tu carrera profesional.

Nos vemos...


10. Referencias


Algunos posts de mi autoria que probablemente te llamen la atención:


Image description

Top comments (0)