Wextensible

NaN en JavaScript

Figura
Figura. Infinito y NaN.

El valor NaN significa Not a Number (no es un número) y se representa en el formato IEEE754 como un caso especial. Se conforma con un signo cero, exponente y fracción con todos los bits puestos a uno. Esto nos dará un valor hexadecimal de 7FFFFFFFFFFFFFFF para el registro del formato.

En JavaScript sólo hay una representación NaN. Pues en el formato IEEE754 sólo es necesario un exponente con todos unos, cualquier signo y una fracción distinta de cero, lo que da lugar a que el número de representaciones de NaN sea muy superior, hasta 253-2 representaciones.

Existe también una propiedad global window.NaN que referencia el mismo valor, pudiendo omitir la referencia window y escribir simplemente NaN. Las siguientes son tres formas de acceder a ese valor:

console.log(Number.NaN); // NaN
console.log(window.NaN); // NaN
console.log(NaN); // NaN
    

Por lo tanto NaN es un tipo number, pues se representa con el mismo formato que el resto de números. Esto responde a una de las dudas planteadas en el primer tema:

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

Generamos un NaN en casos como los siguientes:

console.log("a" - 3); // NaN
console.log(Math.sqrt(-1)); // NaN
console.log(parseFloat("abc")); // NaN
    

Podríamos realizar operaciones con NaN sin que JavaScript lance error, pero el resultado será NaN.

console.log(5+2*NaN); // NaN
    

Esto puede resultar problemático porque podría propagarse un NaN en un conjunto de operaciones y sin que curse error. En este código vamos a sumar la raíces de los elementos de un Array:

let arr = [23, 4, -71, 88];
let suma = 0;
for (let i=0; i<arr.length; i++){
    suma += Math.sqrt(arr[i]);
}
console.log(suma); // NaN
    

Como el tercer elemento es negativo obtenemos un NaN que se propaga hasta el resultado final. Podríamos estar tentados de comprobar si algún resultado parcial es un NaN para no sumarlo:

let arr = [23, 4, -71, 88];
let suma = 0;
for (let i=0; i<arr.length; i++){
    let sq = Math.sqrt(arr[i]);
    if (sq !== NaN) suma += sq;
}
console.log(suma); // NaN
    

Pero eso no funcionará porque NaN es el único valor de JavaScript que no es igual a sí mismo:

//NaN 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    
    

Para saber si un valor no es un número usaremos el método Number.isNaN():

let arr = [23, 4, -71, 88];
let suma = 0;
for (let i=0; i<arr.length; i++){
    let sq = Math.sqrt(arr[i]);
    if (!Number.isNaN(sq)) suma += sq;
}
console.log(suma); // 16.17666304295958
    

El método genérico Number.isNaN() también está disponible en el objeto global window. Recuerde que los métodos y propiedades de window permiten obviar la referencia, así que window.isNaN(num) podemos escribirlo sólo como isNaN(num). El código anterior lo podemos escribir como if (!isNaN(sq)) suma += sq.

Números infinitos en JavaScript y límites POSITIVE_INFINITY y NEGATIVE_INFINITY

El registro del formato es un bit cero o uno para el signo, todos los bits del exponente puestos a uno y todos los bits de la fracción puestos a cero. Esto resultará en los headecimales 7FF0000000000000 para +Infinity y FFF0000000000000 para -Infinity.

Las propiedades Number.POSITIVE_INFINITY y Number.NEGATIVE_INFINITY guardan esos valores ± Infinity. El objeto global también guarda el valor positivo window.Infinity, que podemos escribir sólo como Infinity, valor que es el mismo que Number.POSITIVE_INFINITY.

console.log(Number.POSITIVE_INFINITY); // Infinity
console.log(Number.NEGATIVE_INFINITY); // -Infinity
console.log(window.Infinity); // Infinity
console.log(Infinity); // Infinity
console.log(Number.POSITIVE_INFINITY === Infinity); // true
    

Podemos generar ±Infinity como en los siguientes casos:

console.log(1e500); // Infinity
console.log(Math.pow(1000, 1000)); // Infinity
console.log(1/0); // Infinity
console.log(-1/0); // Infinity
console.log(Math.log(0)); // -Infinity
    

Los valores ±Infinity no dejan de ser números y podemos operar con ellos tal como podríamos hacerlo con el concepto matemático de infinito. En los casos de indeterminaciones obtendremos un NaN:

//Sumar y dividir un número a infinito
console.log(Infinity + 1); // Infinity
console.log(Infinity / 2); // Infinity
//Multiplicar infinito por un numero o infinito
console.log(-2 * Infinity); // -Infinity
console.log(Infinity * Infinity); // Infinity
console.log(Infinity * Number.NEGATIVE_INFINITY); // -Infinity
//Dividir por infinito resulta +0 o -0
console.log(1 / Infinity); // 0
console.log(1 / Number.NEGATIVE_INFINITY); // -0
console.log(-1 / Infinity); // -0
console.log(1 / Math.log(0)); // -0
//Indeterminaciones
console.log(Infinity - Infinity); // NaN
console.log(Infinity / Infinity); // NaN
    

El máximo valor absoluto de un número es MAX_VALUE = 21023 * (2 - ε) = 1.7976931348623157 × 10308. Así si n > MAX_VALUE entonces debería suceder que n = +Infinity, mientras que si n < -MAX_VALUE deberíamos tener n = -Infinity. Vea que decimos debería porque eso no es del todo así tal como expondremos en un apartado posterior. Pero por ahora omitamos eso para resolver unas dudas planteadas:

//¿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
    

Como 2*k > MAX_VALUE resultará en Infinity y así Infinity-k = Infinity siendo k un valor finito, por lo que la comparación Infinity === k fallará:

let k = 1e308;
console.log(2*k); // Infinity
    

Mientras que para la segunda parte 2*k < MAX_VALUE y ahora 2*k-k === k resultará cierto:

k = 1e307;
console.log(2*k); // 2e+307
    

Dónde empieza Infinity y uso del método isFinite()

Aunque en lo anterior hemos razonado que a partir de MAX_VALUE empieza Infinity, en realidad ese máximo valor no puede ser tomado como el límite superior para saber si un número es infinito. Veámos esto con otra de las dudas planteadas:

//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?
    

La respuesta está en el espaciado de los números. El espaciado en ese tramo es de 21023-52 = 21023ε como se observa en esta tabla:

ExponentenRango valores[2n, 2n+1)Espaciado2n-52
···
1022[4.49423283715579e+307, 8.98846567431158e+307)9.9792015476736e+291
1023[8.98846567431158e+307, Infinity)1.99584030953472e+292

Por otro lado el número 21024 es el siguiente al máximo valor 1.11··52··11 × 21023 y no es representable en el formato. Pero podemos restarle un espaciado para calcular el máximo valor:

MAX_VALUE = 21024 - 21023 × ε = 21024 - 21023 × ε = 21023 × (2 - ε) = 1.7976931348623157 × 10308

Como en el formato IEEE754 los números se aproximan al más cercano, cualquier valor en el rango [MAX_VALUE, MAX_VALUE + 21023ε/2) se representará como el inferior MAX_VALUE. Y cualquier número igual o mayor que MAX_VALUE + 21023ε/2 ya se consideran Infinity. Veámos donde empieza Infinity:

let max = Number.MAX_VALUE;
console.log(max); // 1.7976931348623157e+308
let spc = Math.pow(2, 1023-52);
console.log(spc); // 1.99584030953472e+292
console.log(max+spc*0.1); // 1.7976931348623157e+308
console.log(max+spc*0.2); // 1.7976931348623157e+308
console.log(max+spc*0.3); // 1.7976931348623157e+308
console.log(max+spc*0.4);   // 1.7976931348623157e+308
console.log(max+spc*0.4999999999999999); // 1.7976931348623157e+308
console.log(max+spc*0.49999999999999997);// 1.7976931348623157e+308
console.log(max+spc*0.49999999999999998);// Infinity
console.log(max+spc*0.49999999999999999);// Infinity
console.log(max+spc*0.5);   // Infinity
    

Por lo tanto podemos sumar a MAX_VALUE números desde 1 hasta 21023 ε (1/2 - ε/4) = 21022 (ε - ε2/2) sin llegar a Infinity, operándose el resultado de todas esas sumas aproximando al mismo valor inferior MAX_VALUE. El cálculo anterior en función de ε lo deducimos de lo siguiente:

let spc = Math.pow(2, 1023-52);
console.log(spc*0.49999999999999997);    // 9.979201547673598e+291
console.log(spc*(1/2-Math.pow(2, -54))); // 9.979201547673598e+291
console.log(spc*(1/2-Number.EPSILON/4)); // 9.979201547673598e+291
console.log(Math.pow(2, 1022) * (Number.EPSILON -
Math.pow(Number.EPSILON, 2)/2)); // 9.979201547673598e+291
    

En lugar de usar el límite de Infinity en el medio espaciado con el valor calculado antes podemos usar el método Number.isFinite():

console.log(Number.isFinite(Number.MAX_VALUE + 
    1)); // true
console.log(Number.isFinite(Number.MAX_VALUE + 
    9.979201547673598e291)); // true
console.log(Number.isFinite(Number.MAX_VALUE + 
    9.979201547673599e291)); // false
console.log(Number.isFinite(Number.MAX_VALUE + 
    1e292)); //false
    

Como el medio espaciado que vimos es casi 9.8×10291 ≈ 10 × 10308-17 = 10308× 10-16 podríamos variar el último decimal de MAX_VALUE (pues se obtiene con 16 decimales) para ver cuando obtenemos infinito:

console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
console.log(Number.isFinite(1.7976931348623157e+308)); //true
console.log(Number.isFinite(1.7976931348623158e+308)); //true
console.log(Number.isFinite(1.7976931348623159e+308)); //false
    

Por lo tanto Infinity ≥ 1.7976931348623159e+308 tomando 16 dígitos decimales en la fracción, aunque cualquier número mayor que MAX_VALUE y menor que Infinity se representará con el valor de MAX_VALUE = 1.7976931348623157e+308. Observe que la diferencia es el decimal menos significativo, siendo aquí un 7 y un 9 en el anterior. Cualquier valor entre ambos se registrará como MAX_VALUE. Cualquier valor por encima será Infinity.

Los enteros en JavaScript: método isInteger()

Los números enteros en JavaScript se guardan con el mismo formato IEEE754. Por ejemplo, el entero 1234 tiene el formato de coma flotante 1.0011010010··52··00 × 210. Aunque sean enteros se guardan con el mismo formato de coma flotante que el resto. Es como si utilizáramos el formato exponencial de base 10 con 1234 = 1.234×103. Pero incluso un formato exponencial como ese será presentado siempre como un entero sin decimales en JavaScript:

console.log(1.234e3); // 1234
console.log(0.00001234e8); // 1234
    

El máximo entero es un registro IEEE754 con todos los bits de la mantisa puestos a uno y un exponente 52. De eso podemos obtener que el máximo entero representable será ± 253-1:

1.111··52··111 × 252 = 111··53··111 × 2-52 × 252 = 111··53··111 = 253 - 1 = 9007199254740991

Sobre los límites de los enteros comentaremos más cosas en un siguiente apartado. Veamos ahora cómo sabremos si un número es o no un entero. Para eso tenemos el nuevo método genérico de ES6 Number.isInteger(). Aunque lo pasemos en formato de coma flotante el método devolverá que es un entero:

console.log(Number.isInteger(1234)); // true
console.log(Number.isInteger(1.234e3));  // true
console.log(Number.isInteger(0.00001234e8)); // true
console.log(1234 .toString(2)); // 10011010010
console.log(Number.isInteger(0b10011010010));  // true
    

Observe que también evalúa un binario como entero. No debemos olvidar que los formatos binario, hexadecimal u octal son siempre enteros. Un decimal como 2.75 tiene una representación binaria 10.11, pero JavaScript no acepta el punto separador de parte fracccionaria en estos formatos:

console.log(0b10.11); // SyntaxError: missing ) after argument list
    

El cero es un entero, así como cuando realizamos una conversión con el constructor Number. Por otro lado resultaran no enteros NaN e Infinity:

console.log(Number.isInteger(0)); // true
console.log(Number.isInteger(Number("1234"))); // true
console.log(Number.isInteger("1234")); // false
console.log(Number.isInteger(0.5)); // false
console.log(Number.isInteger(NaN)); // false
console.log(Number.isInteger(Infinity)); // false
    

Método parseInt() para convertir a entero

Para convertir un número a entero es probable que hayamos usado el método parseInt(). Realmente es un método del objeto global window, con los que podemos omitir y en lugar de window.parseInt() poner sólo parseInt(). En ES6 se incorpora también como método genérico Number.parseInt(), pero no hay diferencia entre ellos. La razón principal es que el objeto global en los navegadores es window, pero podríamos tener un sistema dónde ese no fuera el objeto global y, por tanto, no tener acceso a parseInt().

La sintaxis del método es parseInt(numero, base). El primer argumento se espera como string, pero si no lo fuera será convertido con toString(). El segundo argumento es la base del sistema numérico que se usará. Por defecto si no se pasa será 10 para el sistema decimal. Puede ir desde 2 para binario hasta 36. Un valor usual es 16 para base hexadecimal.

Aunque parseInt podría decirse que trunca un real a un entero, por ejemplo el real 1234.567 quedaría como 1234, realmente lo que hace es parsear el string buscando los caracteres que componen un número. Empezará leyendo caracteres desde la izquierda. Si la base es 10 esperará encontrar un primer número entre [1, 9] siendo los siguientes cualquiera de [0, 9]. Cuando encuentre el punto verá que no pertenece a ese conjunto y no seguirá el proceso, devolviendo 1234. Podría asimilarse al método Math.floor():

console.log(parseInt(1234.567)); // 1234
console.log(typeof parseInt(1234.567)); // number
console.log(Number.isInteger(parseInt(1234.567))); // true
console.log(Math.floor(1234.567)); // 1234
    

Pero no es una operación matemática, pues aunque Math.floor() también puede trabajar con string, los convierte a número antes de hacer el truncado del número. Vea en estos ejemplos como "1234abc" es convertido al número 1234 con parseInt() pero no con Math.floor():

console.log(parseInt("1234.567")); // 1234
console.log(Math.floor("1234.567")); // 1234
console.log(parseInt("1234abc")); // 1234
console.log(Math.floor("1234abc")); // NaN
    

Si el primer caracter no es de los esperados ya no seguirá leyendo y devolverá NaN. Si el primer caracter es un punto y está dentro de un string nos devolverá NaN. Pero un número como .123 omitiendo el cero antes del punto es un número válido en JavaScript, la conversión a string devuelve "0.123", tras lo cual parseInt() devolverá cero correctamente.

console.log(parseInt("a123")); // NaN
console.log(parseInt(".123")); // NaN
console.log(.123); // 0.123
console.log(.123 .toString()); // "0.123"
console.log(parseInt(.123)); // 0
    

Si usamos números en notación científica como 1.234e2 usará el número 123.4 para convertirlo en string y de ahí obtener el entero 123. Pero si le pasamos el string "1.234e2" lo tomará tal cual produciendo el entero 1:

console.log(parseInt(1.234e2)); // 123
console.log(parseInt("1.234e2")); // 1
    

El segundo argumento de parseInt() especifica la base. Vea como el número 1234 tiene dígitos que son todos permitidos en base 10 y en base 16. Pero en base binaria cuando llega al segundo dígito 2 ya no sigue leyendo devolviendo el primer 1:

console.log(parseInt(1234, 10)); // 1234
console.log(parseInt(1234, 2)); // 1
console.log(parseInt(1234, 16)); // 4660
    

El método parseInt() junto a toString() son adecuados para hacer conversiones de base. Pero hemos de cuidar algunas cosas como en el código siguiente que no obtenemos una adecuada conversión. El número 1234 tiene por binario 10011010010. Por otro lado podemos escribir un binario directamente anteponiendo 0b quedando 0b10011010010. Pero no podemos usar ese formato para convertirlo nuevamente a decimal. Lo mismo pasa con hexadecimal y cualquier otra base:

//Decimal-binario
console.log(1234 .toString(2)); // 10011010010
console.log(0b10011010010); // 1234
//Esta conversión binario a decimal no es correcta
console.log(parseInt(0b10011010010, 2)); // 1
//Decimal-hexadecimal
console.log(1234 .toString(16)); // 4d2
console.log(0x4d2); // 1234
//Esta conversión hexadecimal a decimal no es correcta
console.log(parseInt(0x4d2, 16)); // 4660
    

El problema es que parseInt() espera un string. Si le pasamos el número literal 0b10011010010 lo convertira primero a decimal resultando 1234, luego lo convierte en string y finalmente lo parsea en base 2 como si fuera un binario, resultando que tras el primer dígito finaliza. Con el hexadecimal 0x4d2 pasa lo mismo pues 123416 = 466010. Para evitar esto hemos de pasarle los números en binarios o hexadecimales como string sin los prefijos 0b o 0x:

console.log(parseInt("10011010010", 2)); // 1234
console.log(parseInt("4d2", 16)); // 1234
    

Si omitimos la base, parseInt() puede deducir la hexadecimal si el string empieza por 0x y la octal si empieza por 0. En otro caso tomará la base 10 para realizar la conversión:

//Estas bases no las puede deducir: parseará en base 10
console.log(parseInt("10011010010")); // 10011010010
console.log(parseInt("0b10011010010")); // 0
console.log(parseInt("4d2")); // 4
//Sólo en este caso puede deducir la base 16 (hexadecimal)
console.log(parseInt("0x4d2")); // 1234
    

También reconoce la base octal cuando el número empieza por cero. Si un número entero empieza con un cero JavaScript lo tomará como un número en base octal:

console.log(01234); // 668
console.log(0o1234); // 668
console.log(parseInt(01234)); // 668
    

En modo estricto no se permite un número entero que empiece con cero, dándonos error que octales literales no están permitidos:

console.log(0o1234); // 668
(function() {
    "use strict";
    console.log(01234); // SyntaxError: Octal literals 
                        // are not allowed in strict mode.
})();
    

Siempre deberíamos usar la base porque podrían darse situaciones donde el parseado no sea el esperado. Imaginemos que tenemos un elemento <input> que recoge una cantidad que un usuario nos comunica. Supongamos que esperamos un número entero mayor que cero y menor que 10000, para lo cual disponemos de unos controles al efecto. Como el value de ese elemento es un string le pasamos parseInt() para convertirlo a entero. Es de suponer que los números que se esperan sean decimales, pero si no indicamos base 10 en el parseInt() y se introduce "0x1234" los controles no lo detectarán y tendremos un número cuyo origen no es base 10.

<input type="text" id="inpute" value="0x1234" />
<script>
    let inpute = document.getElementById("inpute");
    let valor = parseInt(inpute.value);
    if (!isNaN(valor) && valor>0 && valor<10000){
        console.log(valor); // 4660
    } else {
        console.log("Cantidad errónea.")
    }
</script>
    

Método isSafeInteger() y valores MAX_SAFE_INTEGER y MIN_SAFE_INTEGER

Empezaremos este apartado resolviendo una duda planteada:

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

Pues eso tiene que ver con los valores significativos MAX_SAFE_INTEGER y MIN_SAFE_INTEGER, cuyos valores son ± 9007199254740991. ¿De dónde sale ese valor? ¿Por qué se le llama entero seguro?

Construyamos el máximo entero con la máxima fracción 111··52··111 (52 bits). Usando el bit implícito y un exponente 52 tendremos el binario en coma flotante 1.111··52··111 × 252. Hágamos estos cálculos para ver de dónde sale el valor 9007199254740991:

1.111··52··111 × 252 = 111··53··111 × 2-52 × 252 = 111··53··111 = 253 - 1 = 9007199254740991
Note que el valor decimal de un binario con todo unos de la forma 111··n··111 es 2n - 1.

El valor anterior 9007199254740991 es el binario 111··53··111, con 53 unos. Si sumamos 1 al valor anterior tenemos 9007199254740992 cuyo binario será 100··54··000, un uno y 53 ceros. El binario en coma flotante de este número es 1.000··53··000 × 253, con 53 bits puestos a cero en la fracción y un exponente de 53. Para el exponente tenemos espacio suficiente hasta el máximo 1023, pero en la fracción nos hemos pasado un bit de los 52 que admite el formato. Por eso el máximo entero es 9007199254740991.

Pero lo curioso es que JavaScript nos deja manejar números mayores que 9007199254740991. Observe estos dos números siguientes al máximo entero seguro:

console.log(9007199254740992); // 9007199254740992
console.log(9007199254740993); // 9007199254740992
console.log(9007199254740992 === 9007199254740993); // true
console.log(9007199254740992 - 9007199254740993); // 0
    

Ambos se almacenan como 9007199254740992. Ambos son el mismo número. La resta es cero. La conclusión es que varios números enteros no seguros pueden tener el mismo formato IEEE754. Dado que tenemos espacio suficiente en el exponente, veámos que pasa cuando usamos el formato IEEE754 para representar el máximo entero seguro y los dos siguientes:

1.111··52··111 × 252 = 253 - 1 = 9007199254740991 1.000··52··0000 × 253 = 1.0 × 253 = 9007199254740992 1.000··52··0001 × 253 = 1.0 × 253 = 9007199254740992

Se observa que el último dígito resaltado no cabe en el formato de la fracción de 52 bits y, por lo tanto, es ignorado. Usando con esa fracción el exponente 53, ambos números darán el mismo resultado.

Alguno de los números enteros no seguros sólo le corresponde un formato, como el que termina en ...994 y ...998 siguientes, pero otros pueden compartir el mismo formato:

console.log(9007199254740992); // 9007199254740992
console.log(9007199254740993); // 9007199254740992
console.log(9007199254740994); // 9007199254740994
console.log(9007199254740995); // 9007199254740996
console.log(9007199254740996); // 9007199254740996
console.log(9007199254740997); // 9007199254740996
console.log(9007199254740998); // 9007199254740998
console.log(9007199254740999); // 9007199254741000
console.log(9007199254741000); // 9007199254741000
    

Así que podemos usar números mayores que el máximo entero seguro +9007199254740991 y menores que el mínimo entero seguro -9007199254740991 sin que JavaScript lance error alguno. Pero para poco nos sirve cuando no podemos asegurar que tengan un único formato IEEE754.

Otra forma de ver esto es mediante el espaciado que expusimos en el apartado sobre Number.EPSILON:

ExponentenRango valores[2n, 2n+1)Espaciado2n-52
···
49[562949953421312, 1125899906842624)0.125
50[1125899906842624, 2251799813685248)0.25
51[2251799813685248, 4503599627370496)0.5
52[4503599627370496, 9007199254740992)1
53[9007199254740992, 18014398509481984)2
54[18014398509481984, 36028797018963970)4
···
1022[4.49423283715579e+307, 8.98846567431158e+307)9.9792015476736e+291
1023[8.98846567431158e+307, Infinity)1.99584030953472e+292

Se observa que para números en el rango [4503599627370496, 9007199254740992) el espaciado es uno. No hay representación posible entre dos números de ese rango. Los rangos por encima producen espaciados cada 2, cada 4, cada 8 y así sucesivamente.

Si hemos de manejar números grandes podemos usar los límites Number.MAX_SAFE_INTEGER y Number.MIN_SAFE_INTEGER. O mejor, para no estar comprobando entre ambos extremos podemos usar el nuevo método Number.isSafeInteger() de ES6:

let num = 9007199254740992;
console.log(Number.isInteger(num) &&
            num >= Number.MIN_SAFE_INTEGER && 
            num <= Number.MAX_SAFE_INTEGER); // false
console.log(Number.isSafeInteger(num)); // false
    

Vemos que con la comprobación de límites también hay que comprobar que sea un entero, mientras que isSafeInteger() hace la doble comparación, que sea entero y que sea seguro. Si quitamos isInteger() y usamos un valor no entero obtenemos que está en los límites, pero en cambio isSafeInteger() resultará falso que es lo que realmente queremos.

let num = 1234.567;
console.log(num >= Number.MIN_SAFE_INTEGER && 
            num <= Number.MAX_SAFE_INTEGER); // true
console.log(Number.isSafeInteger(num)); // false
    

Puede resultar raro el caso que manejemos números enteros tan grandes. Pero pensemos en identificadores de usuario por ejemplo, pudiendo darse el caso de errar en la identificación de un usuario tomándolo por otro. O en números de tarjetas bancarias de nuestros clientes, que también tienen 16 dígitos como el máximo entero seguro. En todos estos casos es mejor manejarlos con string, pues al fin y al cabo más que un número es una secuencia de caracteres numéricos componiendo un identificador.