这是我参与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 (进1,1.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.00011001100(1100循环)... //
复制代码
换成科学计数法就是
1.1001100(1100循环)* 2^-4 // (二进制中2为底数)
复制代码
然后会把整数的1隐藏(永远为1,计算机不存储,隐藏),这就是尾数部分100110011001100110011001100110011001100110011001100110011...
但是由于尾数部分只能存储52位,于是乎52位后面的需要舍弃
这时候按照IEEE 754
的Round to nearest ties to even
规则(向偶数舍入),如下
- 如果舍弃的部分是1xxx, 并且xxx部分有一个为1(也就是大于舍弃的一半),则进1
- 如果舍弃的部分是0xxx(也就是小于舍弃的一半), 则舍弃
- 如果舍弃的部分是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 (进1,1.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
规则进行取舍,导致精度出现误差。