JS 的 Number 类型精度问题

我们知道,Java 语言中的长整型范围为 +2^63-1 ~ -2^63-1,而 JavaScript 的 Number 基本类型采用 64 位浮点型表示,为什么 JS 中整型的最大安全范围不是 +2^63-1 ~ -2^63-1 呢?

Java的 的长整型整数也是采用 64 位表示,除了第一位是符号位,剩下的 63 位都可以表示数字。

而 JavaScript 没有单独的整型类型 (Bigint 除外,且 Bigint 不能参与与 Number 类型的运算),整型、浮点型都统一用 64 位浮点型来表示,采用的是 IEEE754 标准。

IEEE754 规定,64 位的浮点型中,第 1 位表示正负,2~12 位表示指数位 (实际存储的时候必须加上一个偏移值 1023),剩下的 64 - 1- 11 = 52 位表示整数位,因为浮点数采用科学计数法,第一位固定是 1,可以不用表示,但是运算时会加上。

比如:

0 01111111011 0000000000000000000000000000000000000000000000000000

表示的就是 1.0000000000000000000000000000000000000000000000000000 * 2^(1019 - 1023) = 0.0625

Number.Max_Value 与 Number.MAX_SAFE_INTEGER

我们打印 Number 的这两个属性:

1
2
3
4
5
console.log(Number.MAX_VALUE);
console.log(Number.MAX_SAFE_INTEGER);

// 1.7976931348623157e+308
// 9007199254740991

1. Number.Max_Value

其中 Number.Max_Value 就是 Number 类型可以表示的最大浮点数,计算方式如下:

你可以猜到会是:

0 11111111111 1111111111111111111111111111111111111111111111111111,

但这种情况在 IEEE754 标准中表示 NaN,最大的数其实是:

0 11111111110 1111111111111111111111111111111111111111111111111111

转换成二进制的科学计数法表示如下:

1.1111111111111111111111111111111111111111111111111111 * 2^(2046 - 1023)

= 1.1111111111111111111111111111111111111111111111111111 * 2^1023

= (2^53 - 1) * 2^971

我们可以在浏览器调试窗口中验证:

1
2
(Math.pow(2, 53) - 1) * Math.pow(2, 971) // 1.7976931348623157e+308
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE // true

2. Number.MAX_SAFE_INTEGER

现在就可以解释为什么 JS 的最大整数为 +2^53-1 ~ -2^53-1。

因为 IEEE754 浮点数中整数位最大表示的数为:

1111111111111111111111111111111111111111111111111111 = 2^53-1

比这还大一位的数字的表示为:

100000000000000000000000000000000000000000000000000000 = 2^53

在计算机中表示为:

0 10000110101 0000000000000000000000000000000000000000000000000000 0

注意到我们省去掉了一位,按照向偶舍入的规则,不会产生进位。所以这个数还是可以精确表示的,没有问题。

我们再来看看比 MAX_SAFE_INTEGER 大二的数:

100000000000000000000000000000000000000000000000000001

= 1.00000000000000000000000000000000000000000000000000001 * 2^53

在计算机中表示成:

0 10000110101 0000000000000000000000000000000000000000000000000000 1

注意到我们省去掉了一位,按照向偶舍入的规则,还是不会产生进位。这个时候就有问题了,这个数跟刚才那个数竟然是相等的,我们来验证下:

1
2
const a = Number.MAX_SAFE_INTEGER
console.log(a + 1 === a + 2) // true

所以 Number.MAX_SAFE_INTEGER 表示能够准确表示的整数。

进行大数运算时,如果涉及到的的数值超过了 Number.MAX_SAFE_INTEGER,运算就会有误差了,此时的运算最好采用 Bigint 类型。

3. Number.MIN_VALUE

Number.MIN_VALUE 表达的意思是 JavaScript 能够表示最小的正数,及很接近于 0 的数字,数值为:

1
console.log(Number.MIN_VALUE) // 5e-324

0.2 + 0.1 为什么不等于 0.3

这其实是计算机表示浮点数的过程中,由于存储空间的显示,对于某一些浮点数服务精确地表示。

对于十进制转二进制,整数部分除二取余,倒序排列,小数部分乘二取整,顺序排列,所以:

0.1 转化为二进制
0.0 0011 0011 0011 0011 0011 0011 … (0011循环)

0.2 转化为二进制
0.0011 0011 0011 0011 0011 0011 0011 … (0011循环)

然后采用 IEEE754 标准表示:

0.1
指数位: -4;
整数位: 1.1001100110011001100110011001100110011001100110011010 (52位)

0.2
指数位: -3;
整数位: 1.1001100110011001100110011001100110011001100110011010 (52位)

0.1 + 0.2

0.1100110011001100110011001100110011001100110011001101 (52位) 指数位: -3

+

1.1001100110011001100110011001100110011001100110011010 (52位) 指数位: -3

=

10.0110011001100110011001100110011001100110011001100111 (52位) 指数位: -3

=

1.00110011001100110011001100110011001100110011001100111 (53位) 指数位: -2

此时整数位已经溢出了,最后一位为 1,所以进位:

1.0011001100110011001100110011001100110011001100110100 (52位) 指数位: -2

= 1.0011001100110011001100110011001100110011001100110100 * 2 ^ -2

= 0.010011001100110011001100110011001100110011001100110100

= 0.30000000000000004