不看后悔系列,读完这篇文章,你将彻底理解 Javascript 是如何处理数字的,而不仅仅是浮点数的精度问题。

所有使用 Javascript 的开发者都应该要明白这背后的原理,因为写的多了这些问题一定会影响到你的代码。不仅如此,面试也有可能遇到,记得 LeetCode 里有一题需要把整数字符串转成整数,当数字很大的时候就会出现问题。

浮点数在内存里的样子

最近开发的产品需要解析字符串形式的算数表达式,然后计算得出结果,想必会遇到精度问题,就仔细研究了下底层原理,查阅了大量资料发现:大家用的都是同一个例子:

0.1 + 0.2
// 0.30000000000000004

但其实,是不需要算数运算的,很多浮点数都不精确。Javascript 只有一种数字类型,是遵循 IEEE754 标准的 64 位双精度二进制浮点数,其存储格式如下图:

双精度浮点数的内存格式

双精度浮点数的内存格式

其中:

  • sign:为正负符号标志位,记为 s
  • exponent:为指数位,取值范围 [0, 2048),记为 e
  • fraction:为小数位,即有效数字,因为内存中以二进制存储,所以首位一定是1,因此省略以便能表示更多的数。

转十进制数的计算公式为:

转十进制计算公式

转十进制计算公式

其中,b 为二进制小数的第 52 - i 位的值。e 需减去取值范围的中位数 1023。特别的,当 e 等于 0 且小数位均为 0 时,表示 0,当 e 全为 1 且小数位均为 0 时,表示无穷,如果 e 全为 1 且小数位不均为 0 ,那么这不是一个数(NaN)。

对于 0.1 来说,转成二进制小数为:

0.1.toString(2)
// "0.0001100110011001100110011001100110011001100110011001101"

用科学计数法表示:1.100110011001100110011001100110011001100110011001101 x 2^-4,指数为 -4,因此 e 等于 1019,转成 11 位二进制数 01111111011;尾数为 1001100110011001100110011001100110011001100110011010,末尾补齐 0 至 52 位数;正负号标志 s 显然是 0。最终 0.1 在内存中长这样 0011111110111001100110011001100110011001100110011001100110011010,图形化显示为:

图形化 0.1 的存储

图形化 0.1 的存储

浮点数的舍入

小数位其实是 1001 无限循环,根据 IEEE754 的舍入标准 进行了舍入,导致结果会比 0.1 大那么一点点,之所以使用的时候没感觉出来,是因为打印结果的时候舍入了小数点后第 18 位的值。Number 对象里有 toPrecision 方法来指定精度,我们可以来看一下:

0.1.toPrecistion(20);
// "0.10000000000000000555"
0.2.toPrecistion(20);
// "0.20000000000000001110"
0.3.toPrecistion(20);
// "0.29999999999999998890"

解决方法

知道了原因,就能对症下药找到解决的方法,这里我们要分情况讨论:

数据展示:

只需要展示的话,可以 toFixed 或者 toPrecision 选择自己需要的精度,然后再 parseFloat 转成浮点数。需要注意的是,这两个方法均不是四舍五入法,而是上面提过的 IEEE754舍入标准,舍入至最接近的值,如果有 2 个值一样接近,则取偶数值。

数据运算:

如需对数据进行运算,那么一种常用的方法就是,把小数转成整数再进行运算,只要运算过程涉及到的数字不大于 MAX_SAFE_INTEGER ,得到的结果就是“精确”的。

特殊的数字

Javascript 里定义了几个特殊的数字:Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGERNumber.MAX_VALUENumber.MIN_VALUEInfinity。分别是什么意思呢?

  • Number.MAX_SAFE_INTEGER 是最大安全整数,值为 9007199254740991,再大就会出现溢出,导致意外的结果,如:Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 结果为 true
  • Number.MIN_SAFE_INTEGER 同理,值为 -9007199254740991
  • Number.MAX_VALUE 是最大能表示的值 1.7976931348623157e+308,等于2^1024 - 1,加 1 即为 Infinity
  • Number.MIN_VALUE 是最小能表示的正值 5e-324,是最接近 0 的值。
  • Infinity 表示无穷大,有正无穷和负无穷。

这些数的值都是有原因的,可以试着根据上面说的浮点数存储格式来推导这些数:

最大最小整数:

最大的整数会占满小数位部分,一共是 52 位,由于首位必定为 1 被省略了,所以最大整数共有 53 位数字,因此值为 2^53 - 1,最小整数加个负号即可。

无穷大:

标准规定,指数位全为 1 且 小数位全为 0 表示无穷大,理论值为 2^1024

最大可表示的值:

无穷大减 1,即为 2^1024 - 1

最小可表示的正值:

标准规定,指数位全为 0 且 小数位也全为 0 表示 0,因此,最接近零的数就是指数位为 0,小数位最小位为 1,其他为 0,2^-1023 * 2^-51,等于 2^-1074

Math.pow(2, -1074)
// 5e-324

更好的解决方法

不要重复造轮子,有现成的类库为啥不用?

传送门:Mathjsbignumberjs

有用的链接

IEEE754 浮点数可视化工具