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:
Índice
- typeof NaN es number
- "Problemas" de redondeo en enteros y decimales
- Math.max(), Math.min() y el problema de los Infinity
- ¿Sumas o concatenaciones? Arreglos y objetos
- Operaciones con booleanos
- Que carajos es (! + [] + [] + ![]).length
- Sumas y concatenaciones de enteros
- Truthy vs Falsy
- Conclusiones
- 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
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 unundefined
anumber
. - 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
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
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
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
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
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
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
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
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
Esta misma lógica sirve para:
console.log(Math.min()); // Infinity
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
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:
- Si la entrada es un primitivo, entonces devolvemos el mismo valor.
- De lo contrario la entrada es un objeto, entonces aplicamos el método
valueOf
. Si el resultado es primitivo, lo devolvemos. - De lo contrario llamamos al método
toString
. Si el resultado es primitivo, lo devolvemos. - 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()); // []
Aún seguimos obteniendo el mismo arreglo vacío.
- Aplicamos el método
toString
:
console.log([].toString()); // ""
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()); //""
[] + {}
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()); // {}
Obtenemos el mismo objeto vacío.
- Aplicamos el método
toString
:
console.log({}.toString()); // "[object Object]"
Al convertir un objeto a primitivo obtenemos "[object Object]"
La operación entonces se vería así:
console.log("" + "[object Object]"); // "[object Object]"
{} + []
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:
{
}
+[]
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]"
o si usamos variables para realizar la operación:
const obj = {};
const arr = [];
console.log(obj + arr); // "[object Object]"
{} + {}
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)
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 tiponumber
, 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íatrue
. 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
[]
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íafalse
.
Después de todas estas operaciones intermedias el ejercicio tendrá la siguiente pinta:
console.log(( true + "" + false).length);
Esto es mucho más fácil de operar:
-
true + ""
Al concatenartrue
con la cadena vacía, el valor booleano se transforma astring
:
console.log(( "true" + false).length);
-
"true" + false
Otra vez una concatenación de cadena con booleano:
console.log(( "truefalse").length);
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í
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í
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í
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
Here is what you need to know about JavaScript’s Number type
Why is 9999999999999999 converted to 10000000000000000 in JavaScript?
Algunos posts de mi autoria que probablemente te llamen la atención:
Top comments (0)