# 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) 同步更新,欢迎关注😉~