Los números en JavaScript

Figura
Figura. Números en JavaScript.

Siguiendo con el repaso de conceptos básicos de JavaScript y al mismo tiempo estudiando las nuevas incorporaciones de ES6 (EcmaScript 2015), ya iba siendo hora de enfrentarme con los números en JavaScript.

Muchos lenguajes de programación tienen varios tipos para manejar números, como integer para enteros y double para reales. Pero JavaScript sólo tiene un tipo primitivo number para manejar enteros y reales. Se almacena en un registro de 64 bits en formato de coma flotante denominado formato IEEE754.

En JavaScript existe los built-in denominados conjuntamente como Array tipados (Typed Array) con el que podemos crear una estructura sustentada en un buffer de datos binarios. Por ejemplo, Uint32Array nos permite crear una estructura de datos para representar enteros sin signo de 32 bits, lo que equivale al tipo long en otros lenguajes. Pero estos tipos de datos se sustentan en objetos parecidos a Array y no son por tanto tipos primitivos como number.

Son varias las cosas por las que hemos de tener presente las limitaciones del tipo number. Por ejemplo si tuviéramos un tipo integer de 32 bits, el rango de valores iría desde -231 hasta 231-1. Vea que de los 32 bits uno se usa para el signo y el resto para representar los números. Con el tipo number de JavaScript el rango de enteros es mayor, desde -(253-1) hasta 253-1. Es mayor porque usa un registro de 64 bits, pero no todos los bits son usados para representar un entero, como es el caso de integer.

Algunas limitaciones tienen que ver con la representación binaria de los números decimales. Pero otras vienen del propio formato IEEE754 que usa JavaScript que sólo es de 64 bits. Así vemos resultados de operaciones aritméticas que no alcanzan la precisión que podemos obtener con una simple calculadora. O lo díficil que resulta aplicar un método de redondeo para decimales.

En principio tenía la idea de ocupar sólo uno o dos temas sobre esta materia. Pero he tenido que alargarlo más dado que hay muchas cosas que aclarar. Esta es la lista de temas que componen esta serie con una breve explicación de motivos para cada tema:

Lo básico sobre el tipo number de JavaScript

Con el tipo number podemos trabajar con enteros y reales. E incluso en notación científica o exponencial, por ejemplo 1.234×102 se escribiría como 1.234e2:

//Número entero positivo
console.log(typeof 123456); // number
//Número entero negativo
console.log(typeof -123456); // number
//Número real
console.log(typeof 123.456); // number
//Notación científica
console.log(typeof 1.23456e2); // number
console.log(1.23456e2); // 123.456
    

También admite otras bases, como binario, hexadecimal y octal:

//Binarios empiezan por "0b"
console.log(typeof 0b110101000111101); // number
console.log(0b110101000111101); // 27197
//Hexadecimales empiezan por "0x"
console.log(typeof 0x6A3D); // number
console.log(0x6A3D); // 27197
//Octales empiezan por "0o"
console.log(typeof 0o65075); // number
console.log(0o65075); // 27197
    

Los octales también pueden diferenciarse si empiezan con un cero. Pero en modo estricto esto es un error y es necesario anteponer 0o:

console.log(typeof 065075); // number
console.log(065075); // 27197
(function() {
    "use strict";
    console.log(typeof 065075);
    console.log(065075);    
})(); // SyntaxError: Octal literals are not allowed in strict mode.  
    

El built-in Number nos permite crear un nuevo número con el constructor new Number(valor). Si el valor no puede ser convertido a un número nos devolverá NaN. Es una abreviación de Not a Number indicando que no es un número. En el siguiente código creamos un número a partir de la cadena "123". Observe que ahora el tipo es object y no number. Podemos obtener el tipo primitivo almacenado usando el método valueOf().

let num = new Number("123");
console.log(num); // Number {[[PrimitiveValue]]: 123}
console.log(typeof num); // object
console.log(num.valueOf()); // 123
console.log(typeof num.valueOf()); // number
    

En este ejemplo obtenemos un NaN pues no puede convertir "abc" en un número. Observe que NaN es tambien un número:

let num = new Number("abc");
console.log(num); // Number {[[PrimitiveValue]]: NaN}
    

Si prescindimos del operador new el constructor realiza una conversión de datos. En ese caso no devuelve un objeto, sino un tipo primitivo number. Por otro lado ciertas conversiones podrían sorprendernos. Por ejemplo, una cadena vacía o un Array vacío se convierten a cero. Un Array con un único elemento usa ese elemento para convertirlo, pero si tienen más elementos no podrá convertirlo:

let num = Number("");
console.log(Number("")); // 0
num = Number([]);
console.log(num); // 0
num = Number(["123"]);
console.log(num); // 123
num = Number([1,2,3]);
console.log(num); // NaN
num = Number({});
console.log(num); // NaN
num = Number(true);
console.log(num); // 1
num = Number(false);
console.log(num); // 0
    

Number() sin new también nos servirá para convertir una cadena que represente números binarios, hexadecimales u octales:

console.log(Number("0b110101000111101")); // 27197
console.log(Number("0x6A3D")); // 27197
console.log(Number("0o65075")); // 27197
    

Cuando convertimos a number una fecha creada con el built-in Date obtenemos el verdadero valor de la fecha, que no es otra cosa que un entero con el número de milisegundos transcurridos desde el 1 de enero de 1970 y esa fecha:

let fecha = new Date();
console.log(fecha.valueOf()); // 1479506808450
let num = Number(fecha);
console.log(num); // 1479506808450
    

El tipo primitivo number declarado literalmente tendrá una envoltura de objeto Number. Esto nos permite acceder a las propiedades y métodos del built-in. Por ejemplo, podemos acceder al método toFixed() para redondear un número:

let num = 123.45678;
console.log(num.toFixed); // toFixed() { [native code] }
console.log(num.toFixed(2)); // 123.46
    

Dudas con los números en JavaScript

Si queremos entender a fondo el tipo number en JavaScript hemos de conocer detalles sobre la representación binaria de los decimales, el formato IEEE754, detalles sobre la precisión y las reglas de redondeo en ese formato. Eso es lo que intentaremos llevar a cabo en esta serie de temas. El objetivo final es ver cómo JavaScript maneja los números intentando comprender el motivo de aclarar dudas como las siguientes. Los enlaces nos llevan al tema que soluciona esa duda:

Dudas con NaN:

//NaN es un number!!!
console.log(typeof NaN); // number
//Además es el único valor de JavaScript que no es igual a sí mismo!!!
console.log(NaN === NaN); // false
console.log(NaN !== NaN); // true
//¿Y cómo sabré cuando un valor no es un número?
console.log(123 === NaN); // false
console.log("abc" === NaN); // false, cuando debería ser true
    

Dudas con Infinity:

//Infinity es un number!!!
console.log(typeof Infinity); // number

//¿Por qué estas dos expresiones no son iguales para ese valor de k?
let k = 1e308;
console.log(2*k-k === k); // false
//...y sin embargo si lo es para este valor
k = 1e307;
console.log(2*k-k === k); // true

//Si MAX_VALUE es el máximo valor representable en JavaScript
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
//¿Por qué si le sumamos uno no es Infinity?
console.log(Number.MAX_VALUE + 1); // 1.7976931348623157e+308
//Ni este tampoco es Infinity
console.log(Number.MAX_VALUE + 9.979201547673598e291); 
    // 1.7976931348623157e+308
//Y sin embargo a partir del anterior si lo son
console.log(Number.MAX_VALUE + 9.979201547673599e291); // Infinity
console.log(Number.MAX_VALUE + 1e292); // Infinity
//¿Entonces dónde empieza Infinity?
    

Dudas con la precisión:

//¿Por qué 1.7-1.1 no resulta exactamente 0.6?
console.log(1.7-1.1);   // 0.5999999999999999
//Y sin embargo 1.75-1.125 si resulta exactamente 0.625
console.log(1.75-1.125) // 0.625
    

Dudas con resultados de operaciones aritméticas

//¿Cómo se explican estos resultados?
// 1.33 * 1.4 = 1.862
console.log(1.33*1.4); // 1.8619999999999999
// 1.33 * 1.1 = 1.463
console.log(1.33*1.1); // 1.4630000000000003
// 10.7 - 1.12 = 9.58
console.log(10.7-1.12);// 9.579999999999998
// 10.9 - 1.12 = 9.78
console.log(10.9-1.12);// 9.780000000000001
    

Dudas con el número de dígitos decimales:

//Por qué esta resta entre números distintos resulta cero
let m = 0.12345678901234567;
let n = 0.12345678901234566;
console.log(m-n); // 0    
    

Dudas con los números enteros:

//¿Por qué estos dos números enteros distintos se comparan como iguales?
console.log(9007199254740992 === 9007199254740993); // true    
    

Dudas con cero y números muy pequeños:

//¿Por qué hay un cero negativo en JavaScript? ¿Para qué se usa?
console.log(typeof -0); // number
console.log(0 === -0); // true
console.log(-0 > 0); // false
console.log(-0 < 0); // false

//¿Cómo podemos saber si un valor es un cero negativo?
let n = -0;
//Comparando con -0 y +0 no funciona, ambos nos da verdadero 
console.log(n === -0); // true
console.log(n === 0);  // true
//Si usamos esta expresión si funcionará ¿Por qué?
n = -0; console.log(n===0 && 1/n<0); // true
n = 0; console.log(n===0 && 1/n>0); // false

//¿Por qué estas dos expresiones no son iguales para ese valor de k?
let k = 5e-324;
console.log(10*k === 100*(k/10)); // false
//...y sin embargo si lo es para este valor
k = 5e-323;
console.log(10*k === 100*(k/10)); // true
    

Dudas con el método toFixed():

//¿Por qué si 1/3 es 0.333333... con infinitos dígitos 3  
//el metodo toFixed() devuelve los dígitos finales 1483?
console.log((1/3).toFixed(20)); // 0.33333333333333331483
    

Dudas con el redondeo de toFixed():

// El método toFixed() redondea abajo algunas fracciones 0.5
console.log((1.025).toFixed(2)); // 1.02
// Pero otras las redondea arriba ¿Por qué?
console.log((1.125).toFixed(2)); // 1.13
    

Dudas con operaciones aritméticas

//La propiedad distributiva de la multiplicación sobre la suma
//garantiza que a*(b+c) = a*b+a*c, con lo que si a=171, b=1, 
//c=0.015 entonces 171*(1+0.015) = 171 + 171*0.015
//¿Pero no es así con los números en JavaScript?
console.log(171 + 171 * 0.015); // 173.565
console.log(171 * (1 + 0.015)); // 173.56499999999997
    

Duda con redondeos de operaciones: Queremos aplicar el redondeo estándar a los dos resultados anteriores, tal que redondeando a dos dígitos fraccionarios hemos de observar el siguiente tercer dígito de tal forma que si es un 5 o mayor redondearía arriba y en otro caso abajo. Con esa técnica el resultado esperado sería 173.57. Usando toFixed(2) devuelve 173.56 para los dos resultados ¿Entonces cuál sería el método de redondeo adecuado que devolviera el valor correcto 173.57 para ambos resultados de operaciones?

Propiedades y métodos de Number

En el siguiente ejemplo podrá extraer las propiedades y métodos genéricos y del prototipo de Number. Los genéricos se referencian como Number.propiedad mientras que los del prototipo como num.propiedad:

Ejemplo: Propiedades y métodos de Number

propiedades y métodos de Number en este navegador
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Los métodos del prototipo se aplican sobre las instancias de number. Pero hemos de tener cuidado con el punto separador, pues pude confundirse con el punto decimal. Hay que dejar un espacio o rodear con paréntesis el número:

//Dejar espacio después del número o usar paréntesis...
console.log(123 .toFixed(2)); // 123.00
console.log((123).toFixed(2)); // 123.00
//...evita un error de sintaxis 
console.log(123.toFixed(2)); //SyntaxError: Invalid or unexpected token
    

La siguiente lista incorpora enlaces a diferentes partes de estos temas donde se explica cada cosa: