今天来研究下js的精度问题(0.1+ 0.2 不等于0.3)

491 阅读6分钟

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

问题

面试官:你能讲讲js的最大精度整数吗?

面试者: 可以,最大精度整数是Math.pow(2, 53) -1,最小是-Math.pow(2,53) + 1

面试官:那为什么会出现0.1+ 0.2 不等于 0.3的情况?

面试者: 这个是因为js的精度问题。

面试官:什么精度问题,能详细点吗?

面试者:我。。。

以上场景纯属虚构,哈哈哈。

今天来学学js的精度问题。

精度

js主要是用采用IEEE 754 标准,使用64位存储数值,存储是使用二进制格式

然后这64位分为

  • 第1位是符号位(0为正数,1为负数)
  • 之后第2位到第12位(11位)是指数部分
  • 之后的第13位到第64位是(52位)是尾数部分

比如0.1转为64位二进制存储就是

先转成二进制

0.1 * 2 = 0.2 -> 0 
0.2 * 2 = 0.4 -> 0 
0.4 * 2 = 0.8 -> 0 
0.8 * 2 = 1.6 -> 1 (进11.6减一)
0.6 * 2 = 1.2 -> 1 (同上)
0.2 * 2 = 0.4 -> 0 
0.4 * 2 = 0.8 -> 0 
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0 
0.4 * 2 = 0.8 -> 0 
....
// 转换的结果是
0.000110011001100循环)... //

换成科学计数法就是

1.10011001100循环)* 2^-4 // (二进制中2为底数)

然后会把整数的1隐藏(永远为1,计算机不存储,隐藏),这就是尾数部分100110011001100110011001100110011001100110011001100110011...

但是由于尾数部分只能存储52位,于是乎52位后面的需要舍弃

这时候按照IEEE 754Round to nearest ties to even规则(向偶数舍入),如下

  1. 如果舍弃的部分是1xxx, 并且xxx部分有一个为1(也就是大于舍弃的一半),则进1
  2. 如果舍弃的部分是0xxx(也就是小于舍弃的一半), 则舍弃
  3. 如果舍弃的部分是100000(也就是等于舍弃的一半),如果保留的最后一位是1,则进1,因为向偶数舍入, 如果是0,则舍弃,最后保留的都是偶数。

这里它53位后是10011,则1001100110011001100110011001100110011001100110011001直接进1,

等于1001100110011001100110011001100110011001100110011010

对于指数部分的11位,存储的是科学计数法的2的次幂

11位可以存储2 ** 11 = 2048个数,然后需要兼容正数和负数,IEEE754标准规定中间值1023代表的是次幂为0。

上面0.1的次幂是-4,则所以它的指数部分是1023-4 = 1019, 转为二进制就是01111111011

最终0.1换算成64位就是

0 01111111011 1001100110011001100110011001100110011001100110011010

同理0.2换成64位就是

0.2 * 2 = 0.4 -> 0 
0.4 * 2 = 0.8 -> 0 
0.8 * 2 = 1.6 -> 1 (进11.6减一)
0.6 * 2 = 1.2 -> 1 (同上)
0.2 * 2 = 0.4 -> 0 
0.4 * 2 = 0.8 -> 0 
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0 
0.4 * 2 = 0.8 -> 0 

0.0011001100(1100循环)

转成科学计数法
1.10011001100(1100循环) * 2^-3

隐藏整数
10011001100(1100循环)

按照上面的`Round to nearest ties to even`规则(向偶数舍入),进1,尾数部分就是

1001100110011001100110011001100110011001100110011010

指数部分
1023-3 = 1020,转成二进制01111111100

合起来就是

0 01111111100 1001100110011001100110011001100110011001100110011010

二者相加运算

0.1:  0 01111111011 1001100110011001100110011001100110011001100110011010

0.2:  0 01111111100 1001100110011001100110011001100110011001100110011010

因为0.1和0.2的指数部分不相同,需要先转相同再相加,所以指数部分进1,然后尾数部分向左移动一位

0.1:  0 01111111100[整数部分是0]1100110011001100110011001100110011001100110011001101

0.2:  0 01111111100[整数部分是1]1001100110011001100110011001100110011001100110011010

相加得到
0 01111111100[10]0110011001100110011001100110011001100110011001100111

指数位需要加一,然后小数点要左移一位
0 01111111101[1]00110011001100110011001100110011001100110011001100111
             

现在尾数部分现在有53位,然后按照上面的Round to nearest ties to even规则(向偶数舍入),

得到

0  01111111101 0011001100110011001100110011001100110011001100110100

转成十进制等于 0.30000000000000004

总结

主要是因为js采用了IEEE 754标准,然后由于小数部分超过固定位数后会按照Round to nearest ties to even规则进行取舍,导致精度出现误差。

参考

# 也许你知道 0.1 + 0.2 === 0.3 为 false,但是 1.1 + 0.2 === 1.3 呢?