前言:
在看了 JavaScript 浮点数陷阱及解法 和 探寻 JavaScript 精度问题 后,发现没有具体详细的推导0.1+0.2=0.30000000000000004
的过程,因此我写了此文补充下css
正文:html
console.log(0.1+0.2) //0.30000000000000004
复制代码
将 0.1 转为二进制:nginx
没有整数部分
小数部分为 0.1,乘 2 取整,直至没有小数:
0.1 * 2 = 0.2 ...0
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.1 的二进制为:0.0 0011 0011 0011 无限循环0011
git
采用科学计数法,表示 0.1 的二进制:github
//0.00011001100110011001100110011001100110011001100110011 无限循环0011
//因为是二进制,因此 E 表示将前面的数字乘以 2 的 n 次幂
//注意:n 是十进制的数字,后文须要
2^(-4) * (1.1001100110011循环0011)
(-1)^0 * 2^(-4) * (1.1 0011 0011 0011 循环0011)
复制代码
因为 JavaScript 采用双精度浮点数(Double)存储number,因此它是用 64 位的二进制来存储 number 的web
十进制与 Double 的相互转换公式以下:segmentfault
V:表示十进制的结果
SEM:表示双精度浮点数的结果(就是 S 拼 E 拼 M,不是相加)markdown
2^(-4) * (1.1001100110011循环0011)
套用此公式右边,得:app
(-1)^0 * 2^(-4) * (1.1 0011 0011 0011 循环0011)
复制代码
因此,工具
S = 0 //二进制
E = 1019 //十进制
M = 1001100110011循环0011 //二进制
复制代码
双精度浮点数 存储结构以下:
由图可知:
①
S 表示符号位,占 1 位
E 表示指数位,占 11 位
M 小数位,占 52 位(若是第 53 位为 1,须要进位!)
②
//二进制
S = 0 知足条件
//十进制
E = 1019 不知足条件,须要转为 11 位的二进制
//二进制
M = 1001100110011循环0011 不知足条件,须要转为 52 位的二进制
复制代码
① 将 1019 转为 11 位的二进制
//1019
1111111011 ,共 10 位,但 E 要 11 位,因此要在首部补 0
E = 01111111011
复制代码
在线转换工具:在线转换工具(BigNumber时不许确)
② 将1001100110011循环0011
转为 52 位的二进制
//1 0011 0011 0011 循环0011 第53位
1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
第 53 位为 1,要进位,同时舍去第53位及其日后的
M = 1001100110011001100110011001100110011001100110011010 //共 52 位
复制代码
综上:
S = 0
E = 01111111011
M = 1001100110011001100110011001100110011001100110011010
复制代码
拼接 SEM 获得 64 位双精度浮点数:
S E M
0 01111111011 1001100110011001100110011001100110011001100110011010
//合并获得 64 位双精度浮点数
0011111110111001100110011001100110011001100110011001100110011010
复制代码
故 0.1 在 JavaScript 中存储的真实结构为:0011111110111001100110011001100110011001100110011001100110011010
经过 Double相互转换十进制(它是我找获得的有效位数最多的网站) 得:
1.00000000000000005551115123126E-1
等于
1.00000000000000005551115123126 * (10^-1)
等于
0.100000000000000005551115123126
复制代码
也就是说:
0.1
//十进制
至关于
(-1)^0 * 2^(-4) * (1.1001100110011001100110011001100110011001100110011010)
//十进制的值
至关于
0011111110111001100110011001100110011001100110011001100110011010
//Double(双精度)
至关于
0.100000000000000005551115123126
//十进制!
因此用一句话来解释为何JS有精度问题:
简洁版:
由于JS采用Double(双精度浮点数)来存储number,Double的小数位只有52位,但0.1等小数的二进制小数位有无限位,因此当存储52位时,会丢失精度!
考虑周到版:
由于JS采用Double(双精度浮点数)来存储number,Double的小数位只有52位,但除最后一位为5的十进制小数外,其他小数转为二进制均有无限位,因此当存储52位时,会丢失精度!
验证下Double值0011111110111001100110011001100110011001100110011001100110011010
是否等于十进制0.100000000000000005551115123126
:
根据十进制与 Double 的相互转换公式得:
//V = (-1)^S * 2^(E-1023) * (1.M)
//S = 0
//E = 119
//M = 1001100110011001100110011001100110011001100110011010
V = (-1)^0 * 2^(-4) * (1.1001100110011001100110011001100110011001100110011010)
//1.1001100110011001100110011001100110011001100110011010的 Double 值计算过程
//S = 0
//E = 1023,二进制为 01111111111
//M = 1001100110011001100110011001100110011001100110011010
//SEM=0011111111111001100110011001100110011001100110011001100110011010
//转为十进制:1.60000000000000008881784197001E0
= 0.0625 * 1.60000000000000008881784197001
复制代码
用 BigInt 类型来相乘:
625n * 160000000000000008881784197001n
等于
100000000000000005551115123125625n
加上小数点后 33 位,等于
0.100000000000000005551115123125625
发现是四舍五入后的结果,也就是同样的
0.100000000000000005551115123126
复制代码
结果一致,验证正确!
同理,将 0.2 转为二进制(过程略,轮到你来练练手了):
0011 0011 0011 无限循环 0011
复制代码
Double:
//注意第 53 位是 1,须要进位!
(-1)^0 * 2^(-3) * (1. 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010)
S = 0
E = 1020,二进制为 01111111100
M = 1001100110011001100110011001100110011001100110011010
SEM = 0011111111001001100110011001100110011001100110011001100110011010
复制代码
经过 Double相互转换十进制(它是我找获得的有效位数最多的网站) 得:
2.00000000000000011102230246252E-1
等于
0.200000000000000011102230246252
复制代码
也就是说:
0.2
//十进制
至关于
(-1)^0 * 2^(-3) * (1.1001100110011001100110011001100110011001100110011010)
//十进制的值
至关于
0011111111001001100110011001100110011001100110011001100110011010
//Double(双精度)
至关于
0.200000000000000011102230246252
//十进制!
用 BigInt 类型来相加:
100000000000000005551115123126n + 200000000000000011102230246252n
等于
300000000000000016653345369378n
加上小数点一位
0.300000000000000016653345369378
复制代码
等等!好像不等于0.30000000000000004
?0.30000000000000001 6653345369378
保留小数点后 17 位得:0.30000000000000001
再次验证:
0.1 = (-1)^0 * 2^(-4) * (1.1001100110011001100110011001100110011001100110011010)
= 0.00011001100110011001100110011001100110011001100110011010
0.2 = (-1)^0 * 2^(-3) * (1.1001100110011001100110011001100110011001100110011010)
= 0.0011001100110011001100110011001100110011001100110011010
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.01001100110011001100110011001100110011001100110011001110
复制代码
二者相加,结果为:0.01 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 10
转化为 Double,即 SEM:
(-1)^0 * 2^(-2) * (1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100 )
S = 0
E = 1021,二进制为 01111111101
最后的 10 被舍掉,而且进位
M = 0011001100110011001100110011001100110011001100110100
SEM = 0011111111010011001100110011001100110011001100110011001100110100
复制代码
经过 Double相互转换十进制(它是我找获得的有效位数最多的网站) 得:
3.00000000000000044408920985006E-1
等于
0.30000000000000004 4408920985006
复制代码
保留小数点后 17 位得:
0.30000000000000004
复制代码
能够看到,两种不一样的计算过程,致使了计算结果的误差,我制做了一张流程图帮助你们理解:
显然,JavaScript 是按照「验证方法二」去计算 0.1+0.2 的值的,我有两个疑问:
① 为何不用偏差更小的「验证方法一」呢?
这个我暂时不知道,有大佬知道的话麻烦给我留言。。
② 为何「验证方法二」的结果偏差比较大?
蹊跷在 二进制小数相加转成 Double 的过程 上,也就是舍去 53 位,并进位会致使偏差:
进位后的 SEM
SEM = 0011111111010011001100110011001100110011001100110011001100110100
转为十进制
V = 0.300000000000000044408920985006
若是不进位的话
SEM = 0011111111010011001100110011001100110011001100110011001100110011
转为十进制
V = 0.299999999999999988897769753748
复制代码
发现仍是对不上「验证一」的结果,缘由仍是在于 Double 的小数位只能保留到 52 位,截取超出的位数不可避免地会致使偏差,而且较大!
网上找的关于0.1+0.2=0.30000000000000004
的文章都是写的「验证方法二」,我也不知道本身的「验证方法一」是否有错误,恳请看到的读者加以指正。
问题 ② 算解决了,问题 ① 暂不解决,我太累了。。
最后:
感谢你的耐心看完了这篇文章,麻烦给文中参考的文章点个赞,没有他们也不会有这篇文章的诞生,谢谢!
(完)