js浮点数的坑

浮点数在计算机中的表示

  • 日常中,32767 这个数用科学计数法可以写成 3.2767*10^4,3.2767 称为尾数(Mantissa),4 就是指数(exponent)
  • 浮点数在计算机中基于科学技术法来表示的,上面是我们日常用十进制来表示,计算机则是二进制,它的基数是 2 不是 10

一种浮点数格式

假设共有 14bits,5bits 表指数,8bits 表尾数,1bit 表示符号

  • 17=>10001=1.0001*2^4 类比十进制的 1.7*10^5

尾数部分

  • 因为尾数在十进制里面 0<尾数<10,在二进制里面就是 0<尾数<2,那么规定最高位只能是 1
  • 因为尾数默认是 1,所以这个 1 就不用保存了,可以节省一位提高精度。所以尾数部分本来是 10001,这样就只用存储 0001,去掉了 1
  • 需要注意的是,这里我们尾数的有效位数是 8 位,而 128.25=>10000000.01,需要 10 个有效位,而我们的模型中尾数部分是 8 位,算上隐含的最高位 1 也才 9 个有效位,所以 128.25 只能舍去末尾的 1,表示为 10000000.0,其实跟 128 相等。所以浮点数不能做精确比较就是这样的

指数部分

  • 但是指数部分是 4,换算成二进制也就是 100,如果以为是 100 就错了。
  • 因为 0.25=>0.01=1*10^-2,但是这样我们无法用指数表示负数。第一个符号位表示的是整个数的正负
  • 现在广泛采用的是偏移的指数。规定一个偏移值,比如偏移值是 16,实际的指数要加上这个偏移值才可以,这样比 16 大的就是正指数,小的就是负指数。
  • 要表示 0.25,如果偏移值 16,那么指数部分是 14,要表示 17 的指数部分,是 16+5=21,换成二进制就是 10101
1bit 5bits 8bits
sign bit exponent Mantissa
符号位 指数 尾数
0 10101 00010000

js 的浮点数标准

js 的 number 遵循 IEEE 754 标准,使用 64 位固定长度表示,也就是 64 位 double 双精度浮点数(类似的有 float 32 位单精度标准)

  • 大多数语言的小数默认都是遵循这个标准,所以 js 有的问题他们也有,包括 java,ruby,python
  • 64bits 分为三部分
    • 符号 S:1bit,0 表示正数,1 表示负数
    • 指数 E:中间的 11 位存储指数
    • 尾数 M:最后的 52 位是尾数
  • 因为指数 E 有 11 位,取值范围是 2^11=2048,也就是[0,2047],所以约定的偏移值是 1023,[0,1023]是负数

浮点误差

浮点数不是精确存储的

0.1 浮点误差(转成二进制无限循环)

  • 0.1 转成二进制==0.0001100110011001100(1100 循环)=1.100110011001100(1100 循环)x2^-4,因为尾数舍去首位的 1,存的是后面的数,但是最多也只能存 52 位,然后再把有误差的只有 52 位尾数的,转成十进制,就变成了 0.100000000000000005551115123126
  • 0.1+0.2=0.30000000000000004,那是因为把这两个转成二进制(这里就有误差了)后再运算,然后再转回十进制,正好是 0.30000000000000004

为什么 x=0.1 就能得到 0.1

  • 因为尾数的固定长度是 52 位,那么加上省略的一位,最多可以表示的数位 2^53=9007199254740992,对应科学计数尾数是 9.007199254740992,我们只用记住 2^53 就好了
  • 2^53 的长度是 16 位,这也是 js 最多能表示的精度
  • 所以可以近似用 toPrecision(16) 来做精度运算,超过的精度会自动凑整处理
  • Precision 是精确的意思
  • 所以 0.1 转成二进制然后再成十进制是 0.100000000000000005551115123126.toPrecision(16)=0.1000000000000000,去掉末尾的 0 正好是 0.1
  • 所以我们看到的 0.1 并不是 0.1
  • 可以用更高的精度解释:0.1.toPrecision(21) = 0.100000000000000005551

toPrecision vs toFixed

两者都能对多余数字取整,但是,前者是处理精度,精度是从左到右第一个不为 0 的数开始计算。后者是小数点指定位数取整,从小数点开始数起来

  • 有些用 toFixed 用来四舍五入,但是其实是有 bug 的,
  • 1.005.toFixed(2)=1.00 而不是 1.01
  • 因为 1.005 实际上是 1.00499999999999989,4 是进不了 1 的

解决浮点数方案

由于理论上用有限的空间来存储无限的小数是不可能保证精确的,但是我们可以处理一下得到我们精确的结果

数据展示类

  • 1.4000000000000001 这样的数据要展示的话,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示
  • parseFloat(1.4000000000000001.toPrecision(12)) === 1.4
  • 为什么选用 12 作为精度,因为这是一个经验之谈,大部分够用了

数据运算类

  • 要把小数转成整数后再运算