En JavaScript moderno, hay dos tipos de números:
-
Los números regulares en JavaScript son almacenados con el formato de 64-bit IEEE-754, conocido como “números de doble precisión de coma flotante”. Estos números son los que estaremos usando la mayor parte del tiempo, y hablaremos de ellos en este capítulo.
-
Los números BigInt representan enteros de longitud arbitraria. A veces son necesarios porque un número entero regular no puede exceder
253ni ser menor a-253manteniendo la precisión, algo que mencionamos antes en el capítulo Tipos de datos. Como los bigints son usados en áreas muy especiales, les dedicamos un capítulo especial BigInt.
Aquí hablaremos de números regulares. Ampliemos lo que ya sabemos de ellos.
Más formas de escribir un número
Imagina que necesitamos escribir mil millones (En inglés “1 billion”). La forma obvia es:
let billion = 1000000000;
También podemos usar guion bajo _ como separador:
let billion = 1_000_000_000;
Aquí _ es “azúcar sintáctica”, hace el número más legible. El motor JavaScript simplemente ignora _ entre dígitos, así que es exactamente igual al “billion” de más arriba.
Pero en la vida real tratamos de evitar escribir una larga cadena de ceros porque es fácil tipear mal.
En JavaScript, acortamos un número agregando la letra "e" y especificando la cantidad de ceros:
let billion = 1e9; // 1 billion, literalmente: 1 y 9 ceros
alert( 7.3e9 ); // 7.3 billions (tanto 7300000000 como 7_300_000_000)
En otras palabras, "e" multiplica el número por el 1 seguido de la cantidad de ceros dada.
1e3 === 1 * 1000; // e3 significa *1000
1.23e6 === 1.23 * 1000000; // e6 significa *1000000
Ahora escribamos algo muy pequeño. Digamos 1 microsegundo (un millonésimo de segundo):
let mсs = 0.000001;
Igual que antes, el uso de "e" puede ayudar. Si queremos evitar la escritura de ceros explícitamente, podríamos expresar lo mismo como:
let mcs = 1e-6; // cinco ceros a la izquierda de 1
Si contamos los ceros en 0.000001, hay 6 de ellos en total. Entonces naturalmente es 1e-6.
En otras palabras, un número negativo detrás de "e" significa una división por el 1 seguido de la cantidad dada de ceros:
// -3 divide por 1 con 3 ceros
1e-3 === 1 / 1000; // 0.001
// -6 divide por 1 con 6 ceros
1.23e-6 === 1.23 / 1000000; // 0.00000123
// un ejemplo con un número mayor
1234e-2 === 1234 / 100; // 12.34, el punto decimal se mueve 2 veces
Números hexadecimales, binarios y octales
Los números Hexadecimales son ampliamente usados en JavaScript para representar colores, codificar caracteres y muchas otras cosas. Es natural que exista una forma breve de escribirlos: 0x y luego el número.
Por ejemplo:
alert( 0xff ); // 255
alert( 0xFF ); // 255 (lo mismo en mayúsculas o minúsculas )
Los sistemas binario y octal son raramente usados, pero también soportados mediante el uso de los prefijos 0b y 0o:
let a = 0b11111111; // binario de 255
let b = 0o377; // octal de 255
alert( a == b ); // true, el mismo número 255 en ambos lados
Solo 3 sistemas numéricos tienen tal soporte. Para otros sistemas numéricos, debemos usar la función parseInt (que veremos luego en este capítulo).
toString(base)
El método num.toString(base) devuelve la representación num en una cadena, en el sistema numérico con la base especificada.
Ejemplo:
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
La base puede variar entre 2 y 36. La predeterminada es 10.
Casos de uso común son:
-
base=16 usada para colores en hexa, codificación de caracteres, etc.; los dígitos pueden ser
0..9oA..F. -
base=2 mayormente usada para la depuración de operaciones de bit, los dígitos pueden ser
0o1. -
base=36 Es la base máxima, los dígitos pueden ser
0..9oA..Z. Aquí el alfabeto inglés completo es usado para representar un número. Un uso peculiar pero práctico para la base36es cuando necesitamos convertir un largo identificador numérico en algo más corto, por ejemplo para abreviar una url. Podemos simplemente representarlo en el sistema numeral de base36:alert( 123456..toString(36) ); // 2n9c
Por favor observa que los dos puntos en 123456..toString(36) no son un error tipográfico. Si queremos llamar un método directamente sobre el número, como toString del ejemplo anterior, necesitamos ubicar los dos puntos .. tras él.
Si pusiéramos un único punto: 123456.toString(36), habría un error, porque la sintaxis de JavaScript espera una parte decimal después del primer punto. Al ver el segundo punto, JavaScript reconoce que la parte decimal está vacía y le sigue un método.
También podríamos escribir (123456).toString(36).
Redondeo
Una de las operaciones más usadas cuando se trabaja con números es el redondeo.
Hay varias funciones incorporadas para el redondeo:
Math.floor- Redondea hacia abajo:
3.1se convierte en3, y-1.1se hace-2. Math.ceil- Redondea hacia arriba:
3.1torna en4, y-1.1torna en-1. Math.round- Redondea hacia el entero más cercano:
3.1redondea a3,3.6redondea a4; los casos medios3.5redondea a4, y-3.5redondea a-3. Math.trunc(no soportado en Internet Explorer)- Remueve lo que haya tras el punto decimal sin redondear:
3.1torna en3,-1.1torna en-1.
Aquí, la tabla que resume las diferencias entre ellos:
Math.floor |
Math.ceil |
Math.round |
Math.trunc |
|
|---|---|---|---|---|
3.1 |
3 |
4 |
3 |
3 |
3.5 |
3 |
4 |
4 |
3 |
3.6 |
3 |
4 |
4 |
3 |
-1.1 |
-2 |
-1 |
-1 |
-1 |
-1.5 |
-2 |
-1 |
-1 |
-1 |
-1.6 |
-2 |
-1 |
-2 |
-1 |
Estas funciones cubren todas las posibles formas de lidiar con la parte decimal de un número. Pero ¿si quisiéramos redondear al enésimo n-th dígito tras el decimal?
Por ejemplo, tenemos 1.2345 y queremos redondearlo a 2 dígitos obteniendo solo 1.23.
Hay dos formas de hacerlo:
-
Multiplicar y dividir.
Por ejemplo, para redondear el número a dos dígitos tras el decimal, podemos multiplicarlo por
100, llamar la función de redondeo y entonces volverlo a dividir.let num = 1.23456; alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 -
El método toFixed(n) redondea el número a
ndígitos después del punto decimal y devuelve una cadena que representa el resultado.let num = 12.34; alert( num.toFixed(1) ); // "12.3"Redondea hacia arriba o abajo al valor más cercano, similar a
Math.round:let num = 12.36; alert( num.toFixed(1) ); // "12.4"Ten en cuenta que el resultado de
toFixedes una cadena. Si la parte decimal es más corta que lo requerido, se agregan ceros hasta el final:let num = 12.34; alert( num.toFixed(5) ); // "12.34000", con ceros agregados para dar exactamente 5 dígitosPodemos convertirlo a
numberusando el operador unario más+o llamando aNumber(). Por ejemplo,+num.toFixed(5).
Cálculo impreciso
Internamente, un número es representado en formato de 64-bit IEEE-754, donde hay exactamente 64 bits para almacenar un número: 52 de ellos son usados para almacenar los dígitos, 11 para almacenar la posición del punto decimal, y 1 bit es para el signo.
Si un número es verdaderamente grande, puede rebasar el almacén de 64 bit y obtenerse el valor numérico Infinity:
alert( 1e500 ); // Infinity
Lo que puede ser algo menos obvio, pero ocurre a menudo, es la pérdida de precisión.
Considera este (¡falso!) test de igualdad:
alert( 0.1 + 0.2 == 0.3 ); // false
Es así, al comprobar si la suma de 0.1 y 0.2 es 0.3, obtenemos false.
¡Qué extraño! ¿Qué es si no 0.3?
alert( 0.1 + 0.2 ); // 0.30000000000000004
¡Ay! Imagina que estás haciendo un sitio de compras electrónicas y el visitante pone $0.10 y $0.20 en productos en su carrito. El total de la orden será $0.30000000000000004. Eso sorprendería a cualquiera…
¿Pero por qué pasa esto?
Un número es almacenado en memoria en su forma binaria, una secuencia de bits, unos y ceros. Pero decimales como 0.1, 0.2 que se ven simples en el sistema decimal son realmente fracciones sin fin en su forma binaria.
alert(0.1.toString(2)); // 0.0001100110011001100110011001100110011001100110011001101
alert(0.2.toString(2)); // 0.001100110011001100110011001100110011001100110011001101
alert((0.1 + 0.2).toString(2)); // 0.0100110011001100110011001100110011001100110011001101
¿Qué es 0.1? Es un uno dividido por 10, 1/10, un décimo. En sistema decimal es fácilmente representable. Compáralo con un tercio: 1/3, que se vuelve una fracción sin fin 0.33333(3).
Así, la división en potencias de diez garantizan un buen funcionamiento en el sistema decimal, pero divisiones por 3 no. Por la misma razón, en el sistema binario la división en potencias de 2 garantizan su funcionamiento, pero 1/10 se vuelve una fracción binaria sin fin.
Simplemente no hay manera de guardar exactamente 0.1 o exactamente 0.2 usando el sistema binario, así como no hay manera de guardar un tercio en fracción decimal.
El formato numérico IEEE-754 resuelve esto redondeando al número posible más cercano. Estas reglas de redondeo normalmente no nos permiten percibir aquella “pequeña pérdida de precisión”, pero existe.
Podemos verlo en acción:
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
Y cuando sumamos dos números, sus “pérdidas de precisión” se acumulan.
Y es por ello que 0.1 + 0.2 no es exactamente 0.3.
El mismo problema existe en muchos otros lenguajes de programación.
PHP, Java, C, Perl, Ruby, dan exactamente el mismo resultado, porque ellos están basados en el mismo formato numérico.
¿Podemos resolver el problema? Seguro, la forma más confiable es redondear el resultado con la ayuda de un método. toFixed(n):
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"
Ten en cuenta que toFixed siempre devuelve un string. Esto asegura que tiene 2 dígitos después del punto decimal. Esto es en verdad conveniente si tenemos un sitio de compras y necesitamos mostrar $0.30. Para otros casos, podemos usar el + unario para forzar un número:
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
También podemos multiplicar temporalmente por 100 (o un número mayor) para transformarlos a enteros, hacer las cuentas, y volverlos a dividir. Como hacemos las cuentas con enteros el error se reduce, pero aún lo tenemos en la división:
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
Entonces el enfoque de multiplicar/dividir reduce el error, pero no lo elimina por completo.
A veces podemos tratar de evitar los decimales del todo. Si estamos tratando con una tienda, podemos almacenar precios en centavos en lugar de dólares. Pero ¿y si aplicamos un descuento de 30%? En la práctica, evitar la parte decimal por completo es raramente posible. Simplemente se redondea y se corta el “rabo” decimal cuando es necesario.
Prueba ejecutando esto:
// ¡Hola! ¡Soy un número que se autoincrementa!
alert( 9999999999999999 ); // muestra 10000000000000000
Esto sufre del mismo problema: Una pérdida de precisión. Hay 64 bits para el número, 52 de ellos pueden ser usados para almacenar dígitos, pero no es suficiente. Entonces los dígitos menos significativos desaparecen.
JavaScript no dispara error en tales eventos. Hace lo mejor que puede para ajustar el número al formato deseado, pero desafortunadamente este formato no es suficientemente grande.
Otra consecuencia peculiar de la representación interna de los números es la existencia de dos ceros: 0 y -0.
Esto es porque el signo es representado por un bit, así cada número puede ser positivo o negativo, incluyendo al cero.
En la mayoría de los casos la distinción es imperceptible, porque los operadores están adaptados para tratarlos como iguales.
Tests: isFinite e isNaN
¿Recuerdas estos dos valores numéricos especiales?
Infinity(y-Infinity) es un valor numérico especial que es mayor (menor) que cualquier otra cosa.NaN(“No un Número”) representa un error.
Ambos pertenecen al tipo number, pero no son números “normales”, así que hay funciones especiales para chequearlos:
-
isNaN(value)convierte su argumento a número entonces testea si esNaN:alert( isNaN(NaN) ); // true alert( isNaN("str") ); // truePero ¿necesitamos esta función? ¿No podemos simplemente usar la comparación
=== NaN? Desafortunadamente no. El valorNaNes único en que no es igual a nada, incluyendo a sí mismo: