# JavaScript 浮点数陷阱

# 前言
;JavaScript中的浮点数经常会有奇怪的运算结果,例如0.1 + 0.2 != 0.3或者是1.005.toFixed(2)结果为1.00,又或者Number.MAX_VALUE与Number.MAX_SAFE_INTEGER的区别等等。
此处对JavaScript浮点数的存储标准和以上疑问做了比较细致的整理,希望对你有用。
# IEEE 754
;JavaScript与其它语言不同,Number类型是不区分整型和浮点的。对于所有的数字包括整数和小数相同存储,遵循IEEE 754的双精度标准,64位固定长度,也即是常说的double类型。
由于整数和小数都采用64位存储,对于内存来说整型和浮点型也就没有区别了。
位运算上,会将操作数转为
32位有符号数,小数部分直接丢弃
;64位比特包括三个部分。
- 符号位(
S,sign):第63位,0表示正数,1表示负数 - 指数位(
E,exponent):第52到62位,共11位,取值范围为0~2047。但是指数位是可以为负数的,偏移2023后取值范围变为[-1023, 1024] - 尾数位(
M,mantissa):第0到51位,共52位。

计算公式为。
;11.25的64位表示。
- 将整数和小数部分转换成二进制,即
11.25 = 1011.01 - 移动小数点,使其位于第
1、2位之间,规范化为1.01101 * 2 ^ 3 11.25为正数,S = 0。另外指数为3,则E = 1023 + 3 = 1026,即100 0000 0010- 舍去整数部分
1,剩下尾数部分01101,空位补0,即0110 1000 ... 0000(52位) 11.25的64位浮点数的二进制表示为0 10000000010 01101000...0000

规范化后的整数部分必然为
1,存储时可以省略,只记录小数点之后的部分, 也就节约了一位内存
# 就近舍入
在四舍五入中,0 ~ 9十个数,0比较特殊,不会存在舍去的情况,舍不舍去都是当前数。而剩下的9个数中,1 ~ 4舍去,5 ~ 9上入,概率上舍去为4 / 9,上入为5 / 9,因此并不是公平的。
就近舍入,或者叫银行家舍入,是四舍六入五成双。即1 ~ 4舍去,6 ~ 9上入,5的情况则看前一位是奇数还是偶数,偶数则舍去,奇数则上入,因此概率都是50%,更加合理。
二进制中的就近舍入,其中1001大于1000则上入1,0111小于1000则舍去,而对于1000的情况,看前一位是奇数还是偶数,若为0则舍去,为1则上入1。
1.001 1001 // 1.010
1.001 0111 // 1.001
1.001 1000 // 1.010
1.100 1001 // 1.101
1.100 0111 // 1.100
1.100 1000 // 1.100
# Number.MAX_VALUE
;Number.MAX_VALUE (opens new window) 即JavaScript中所能表示的最大数值。
按照IEEE 754的64位标准,可以明显想到以下表示。
0 11111111111 1111111111111111111111111111111111111111111111111111
但是注意指数位全为1用来表示NaN或Infinity,因此指数位最大为111 1111 1110。
0 11111111110 1111111111111111111111111111111111111111111111111111
转为十进制。
1.1111111111111111111111111111111111111111111111111111 * 2 ^ (2046 - 1023)
= 1.1111111111111111111111111111111111111111111111111111 * 2 ^ 1023
= 1 1111111111111111111111111111111111111111111111111111 * 2 ^ (1023 - 52)
= 1 1111111111111111111111111111111111111111111111111111 * 2 ^ 971
= (2 ^ 53 - 1) * 2 ^ 971
= 1.7976931348623157e+308
也即是Number.MAX_VALUE。
(Math.pow(2, 53) - 1) * Math.pow(2, 971) // 1.7976931348623157e+308
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE // true
# Number.MAX_SAFE_INTEGER
;Number.MAX_SAFE_INTEGER (opens new window) 表示JavaScript中最大的安全整数。
;Number.MAX_SAFE_INTEGER二进制表示,注意指数刚好为52。
0 10000110011 1111111111111111111111111111111111111111111111111111
转为十进制。
1.1111111111111111111111111111111111111111111111111111 * 2 ^ (1075 - 1023)
= 1.1111111111111111111111111111111111111111111111111111 * 2 ^ 52
= 1 1111111111111111111111111111111111111111111111111111
= 2 ^ 53 - 1
= 9007199254740991
然后来说说为什么叫安全整数,指的是当前整数转换为二进制时,可以完全存储在尾数位中,不会发生舍入。
而Number.MAX_SAFE_INTEGER + 1和Number.MAX_SAFE_INTEGER + 2都会存在舍去的情况。
;Number.MAX_SAFE_INTEGER + 2,9007199254740993的二进制表示。
100000000000000000000000000000000000000000000000000001
规范化。
1.00000000000000000000000000000000000000000000000000001 * 2 ^ 53
由于指数位只能容纳52位,低位为1且前一位为0,就近舍入时会被舍去。
0000000000000000000000000000000000000000000000000000(1)
0000000000000000000000000000000000000000000000000000
;64位浮点数表示。
0 10000110100 0000000000000000000000000000000000000000000000000000
另外Number.MAX_SAFE_INTEGER + 1,也会存在舍去的情况。
;9007199254740992的浮点数表示。
0 10000110100 0000000000000000000000000000000000000000000000000000
因此会造成以下结果。
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true
# 0.1 + 0.2 != 0.3
# 0.1
先来将0.1转换为二进制,连续乘以2并取整。
0.1 * 2 = 0.2 ----- 整 0 余 0.2
0.2 * 2 = 0.4 ----- 整 0 余 0.4
0.4 * 2 = 0.8 ----- 整 0 余 0.8
0.8 * 2 = 1.6 ----- 整 1 余 0.6
0.6 * 2 = 1.2 ----- 整 1 余 0.2
0.2 * 2 = 0.4 ----- 整 0 余 0.4
;0.1的二进制表示,0011将无限循环下去。
0.0 0011 0011 0011 (0011)
规范化,另外0.1为正数,S = 0,指数为-4 + 1023 = 1019,即011 1111 1011。
1.1 0011 0011 (0011) * 2 ^ -4
尾数位最多存储52位,且采用就近舍入模式,向前进1。
1001100110011001100110011001100110011001100110011001(10011)
1001100110011001100110011001100110011001100110011010
;0.1的64位浮点数表示。
0 01111111011 1001100110011001100110011001100110011001100110011010

# 0.2
;0.2的二进制。
0.0011 0011 0011 (0011)
规范化0.2,S = 0,指数为-3 + 1023 = 1020,即011 1111 1100。
1.1 0011 0011 (0011) * 2 ^ -3
存储52位,其余舍去,向前进1。
1001100110011001100110011001100110011001100110011001(10011)
1001100110011001100110011001100110011001100110011010
;0.2的64位浮点数表示。
0 01111111100 1001100110011001100110011001100110011001100110011010

因此实际上0.1和0.2转换为64位的双精度浮点数时,都存在精度损失。
0 01111111011 1001100110011001100110011001100110011001100110011010 // 0.1
0 01111111100 1001100110011001100110011001100110011001100110011010 // 0.2
浮点数运算三步骤,对阶、求和、规范化
# 对阶
;0.1和0.2的指数部分阶次不一致,要先统一阶次,且遵循小阶向大阶看齐的原则。
比如十进制的1.5 * 10 ^ 10 + 1.23 * 10 ^ 13,保留小数点前后1位。
- 大阶向小阶看齐时,即
1.5 * 10 ^ 10 + 1230 * 10 ^ 10,结果为1231.5 * 10 ^ 10,规则舍弃后为1.5 * 10 ^ 10 - 小阶向大阶看齐时,即
0.0015 * 10 ^ 13 + 1.23 * 10 ^ 13,结果为1.2315 * 10 ^ 13,规则舍弃后为1.2 * 10 ^ 13
明显看到小阶向大阶更能接近实际结果。另外阶次若加1,尾数位就要右移1位,阶次相同时对阶完成。
为什么阶次加1尾数位就右移1位呢?
;0.1的64位浮点数表示,值为1.1001 1001 ... 1001 1001 1010 * 2 ^ -4。
0 01111111011 (1.)1001100110011001100110011001100110011001100110011010
此处若阶次加1,要保持值大小不变,小数点要左移1位,即0.1 1001 1001 ... 1001 1001 1010 * 2 ^ -3,也就相当于尾数位右移1位。右移后空位补1,也就是省略的整数部分的1。注意若还要右移,空位只能补0,原因在于整数部分后续都是0了。
0 01111111110 (0.)1100110011001100110011001100110011001100110011001101(0)
因此对阶过程如下,由于低位为0,可以省去。
0 01111111011 1001100110011001100110011001100110011001100110011010 // 0.1
0 01111111100 100110011001100110011001100110011001100110011001101(0) // 阶次加 1,尾数位右移 1 位
0 01111111100 1100110011001100110011001100110011001100110011001101 // 空位补 1
注意尾数位右移是将低位移出,会损失一定的精度,为了减小误差,将保留若干移出的位,在以后的规范化时再做舍入
# 求和
阶数相同,开始求和。
0 01111111100 1100110011001100110011001100110011001100110011001101 // 0.1
+ 0 01111111100 1001100110011001100110011001100110011001100110011010 // 0.2
更好理解的方式,注意尾数位产生了进位。
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
10.0110011001100110011001100110011001100110011001100111
# 规范化
求和结果为10.0 1100 1100 ... 1100 1100 111 * 2 ^ -3,即1.00 1100 1100 ... 1100 1100 111 * 2 ^ -2。
1.00110011001100110011001100110011001100110011001100111
由于指数位只能容纳52位,就近舍入后向前进1。
0011001100110011001100110011001100110011001100110011(1)
0011001100110011001100110011001100110011001100110100
;IEEE 754的双精度64位表示。
0 01111111101 0011001100110011001100110011001100110011001100110100
即1.0011 0011 ... 0011 0011 0100 * 2 ^ -2,转换为十进制也就是0.30000000000000004。
# 小结
要明确的是,诸如0.1、0.2之类的数,虽然在十进制中能非常清晰地表达,但是在二进制中却是无穷尽的,无法精确表示。而JavaScript遵循IEEE 754的双精度标准,仅64位,进制的转换中要舍弃低位来存储,因此必然存在精度丢失。
另外在相加的过程中,对阶、求和、规范化都可能存在舍入,也会存在精度的丢失。
# 1.005.toFixed(2)
;1.005的二进制表示,0000 1010 0011 1101 0111将无限循环下去。
1.000 (0000 1010 0011 1101 0111)
;IEEE 754的双精度64位表示。
0 01111111111 0000000101000111101011100001010001111010111000010100(0)
0 01111111111 0000000101000111101011100001010001111010111000010100
;1.005截断后面位数后,已经小于1.005,利用 Number.prototype.toPrecision (opens new window) 来看下1.005的20位精度。

因此toFixed保留两位小数,四舍五入时。
1.005.toFixed(2) // 1.00
# Number.MAX_VALUE + 1 不是 Infinity
# Number.MAX_VALUE + 1 === Number.MAX_VALUE
;Number.MAX_VALUE与1的64位浮点数表示为。
0 11111111110 1111111111111111111111111111111111111111111111111111 // Number.MAX_VALUE
0 01111111111 0000000000000000000000000000000000000000000000000000 // 1
相加对阶时1的阶数将由0升到1023,尾数位将右移1023位。
0 11111111110 1111111111111111111111111111111111111111111111111111 // Number.MAX_VALUE
+ 0 11111111110 0000000000000000000000000000000000000000000000000000(000...0001) // 1
求和。
1.1111111111111111111111111111111111111111111111111111
+ 0.0000000000000000000000000000000000000000000000000000(000...0001)
1.1111111111111111111111111111111111111111111111111111(000...0001)
规范化时低位将舍弃,实际相当于Number.MAX_VALUE加0,所以会有以下结果。
Number.MAX_VALUE + 1 === Number.MAX_VALUE // true
# 2 ^ 970
那到底Number.MAX_VALUE加上多少等于Infinity呢?
按照IEEE 754的规范,只要大于等于以下数,就会被表示为Infinity。
;64位浮点数中,Emax为1023,p为53。
2 ^ 1023 * (2 - 2 ^ (1 - 53) / 2)
= 2 ^ 1023 * (2 - 2 ^ -53)
= 2 ^ 970 * (2 ^ 54 - 1)
此结果与Number.MAX_VALUE差值为。
(2 ^ 54 - 1) * 2 ^ 970 - (2 ^ 53 - 1) * 2 ^ 971
= 2 ^ 970
因此Number.MAX_VALUE至少加上2 ^ 970才能等于Infinity。
# Number.MAX_VALUE + 2 ^ 970 === Infinity
;Number.MAX_VALUE与2 ^ 970的64位浮点数表示。
0 11111111110 1111111111111111111111111111111111111111111111111111 // Number.MAX_VALUE
0 11111001001 0000000000000000000000000000000000000000000000000000 // 2 ^ 970
相加对阶时2 ^ 970阶数将由970升到1023,差值为53,尾数位将右移53位。
0 11111111110 1111111111111111111111111111111111111111111111111111 // Number.MAX_VALUE
+ 0 11111111110 0000000000000000000000000000000000000000000000000000(1) // 2 ^ 970
求和,就近舍入时,低位舍入,向前进1。
1.1111111111111111111111111111111111111111111111111111
+ 0.0000000000000000000000000000000000000000000000000000(1)
1.1111111111111111111111111111111111111111111111111111(1)
10.0000000000000000000000000000000000000000000000000000
注意尾数位产生了进位,指数位则加1,尾数位右移1位。
结果的64位浮点数表示,而以下表示实际就是Infinity的64位浮点数表示。
0 11111111111 0000000000000000000000000000000000000000000000000000
# 参考
- JavaScript 浮点数陷阱及解法 (opens new window)
- 0.30000000000000004 (opens new window)
- 0.1 + 0.2 为什么不等于 0.3? (opens new window)
- 为什么说 32 位浮点数的精度是 7 位有效数 (opens new window)
- 为什么 Number.MAX_VALUE + 1 不是 Infinity? (opens new window)
# 🎉 写在最后
🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!
手动码字,如有错误,欢迎在评论区指正💬~
你的支持就是我更新的最大动力💪~
GitHub (opens new window) / Gitee (opens new window)、GitHub Pages (opens new window)、掘金 (opens new window)、CSDN (opens new window) 同步更新,欢迎关注😉~