JavaScript 浮点数运算防坑指南

1,068 阅读3分钟

浮点数运算出现的问题

如果我们运算 0.1 + 0.2 ,期望的结果当然是 0.3 。然而。。。

这个坑只发生在浮点数运算上。(所以放心吧,1 + 1 还是等于 2 的)。。。

难道我的数学白学了嘛?

你的数学肯定没白学,但是要明白为什么得先学一下计算机是怎么存数字的

首先,所谓 浮点数 就是小数的二进制表示法。(由符号位,指数位,尾数位 构成)

其中,指数位体现 取值范围 ,尾数位体现 计算精度。尾数位越多能表示的数就越大,计算精度也越高

浮点数中有 半精度(mini) 单精度(float)双精度(double) ,它们分别长成这样:

  • 半精度:1位符号,5位指数,10位小数

  • 单精度:1位符号,8位指数,23位小数

  • 双精度:1位符号,11位指数,52位小数

(JS采用 双精度浮点数,遵循 IEEE 754 标准)

以上只是表示法,实际的值是这样计算的:

  • 符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
  • 指数位E:中间的 11 位存储指数(exponent),用来表示次方数
  • 尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零

所以,计算机存储的数字其实是有精度限制的。具体来说,单精度的小数位在计算机中只有23位(二进制),换算到十进制只能百分百保证6位十进制数字的精确度。不能百分百保证7位的精度运算。超过该精度(二进制23位,十进制6位)的小数运算将会被截取,造成精度损失和计算结果的不准确。

同理,双精度,小数位是52位(二进制),换算为十进制则只能百分百能保证15位。

例如,0.1 转成二进制的话,其实是0.0001100110011001100(1100无限循环) 科学计数法就是 1.100110011001100x2^-4 。由-4 反推指数位E等于 (-4 + (2^11-1)/2)= 1019 ,转为二进制就是 1111111011

所以 0.1 在JS的世界里就是这个样子:

0(符号位) 1111111011(指数位) 1001100110011001100110011001100110011001100110011010(尾数位)

这一长串儿0和1(011111110111001100110011001100110011001100110011001100110011010)代表的就是你输入的0.1

然而这玩意儿转换成 十进制 的时候就成了 0.100000000000000005551115123126

所以,这就是为什么你明明输入的是 0.1,计算的时候却得不到正确结果的原因。因为它看似是个0.1其实它根本就不是0.1

这可咋整?

好了,我们终于知道,由于进制和精度的问题,计算机世界里的计算和我们在草纸上做加减法并不是一回事儿。

那么。。怎么办呢?

单论JS来说,这里推荐几个让人省心的库,可以让你的生活幸福美满

  1. binnumber.js(人如其名,可以处理大数计算,当然普通的运算也不在话下)
  2. numbers.js(大而全的api,应付各种数学计算,什么sin, cos, 微积分。。反正什么都有,然而实际上你只用base里的加减乘除。。。)
  3. number-precision(国产良心,只有1k的小巧可人儿,简单极少的api,只有加减乘除。由阿里的同学倾情贡献~)

好啦~总之,问题就是这么来的,也就是这么解决的。

打完,收工 :)