0.前言
最近在看计算机组成原理的浮点数部分,突然想起之前看过的一道快手面试题
为什么JS中0.1+0.2不等于0.3,应该如何解决?
这里我们可以借这道题来说一下JS的精度问题
1.JS数的储存
二进制和浮点数和定点数
首先计算机里面的数据肯定以二进制形式存储
对于同一段二进制码,不同的解读方式肯定有不同的意义
对于小数,我们有定点数和浮点数两种表示方法
目前计算机大多用浮点数,精度高,表示范围大
一个数以浮点数二进制码形式储存,我们从二进制浮点数码中能算出表达的二进制,然后二进制又可以得到相应的十进制,这就是他们的转化关系
补充下浮点数相对于定点数的优缺点:
优点 : 表示范围大
缺点 : 计算效率低,有误差
复习浮点数
我们复习一下计组中对浮点数的介绍 这里以32位为例
如上图,32位二进制码中有三个部分,符号位,指数位,尾数位
浮点数计算公式:
从左往右看有三个部分,符号位指数位,尾数位
(1)(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
(2)2^E表示指数位。
(3)M表示有效数字,大于等于1,小于2。
这里有两个注意点,1.M由于恒定为1.xxx,所以默认省略1
2.E不全为0或不全为1。这时E=E-127或者E=E-1023(64位)
我们按上面的规则算一下为什么图中0011111000100...00表示的0.15625
首先指数位置01111100表示的是124 则E=124-127=-3
然后我们看尾数,尾数位为1.01(加上了隐藏的1)
所以v=1.01*2的-3次方=0.00101
注意这个是二进制结果,转化为十进制的就是2(-3次方)+2(-5次方)=0.125+0.125*0.25=0.15625
JS中数值
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1
与1.0
是相同的,是同一个数。
我们看一下64位的JS数字是怎么储存的
1位符号位 11位指数位 52位尾数位
JS中1和0.1的表示方法
在 http://www.binaryconvert.com/... 这个网站上我们可以找到一个数的二进制浮点数表示
我们手动按上面的方法算一下,并且验证看对不对
1的表示方法
1对应的二进制1.00000
那么1对应的浮点数应该长成这样
1.0* 2的0次方
指数为0 尾数为1.0 所以指数实际是1023(1023-1023=0) 尾数是0000000000000...00
看来我们算的是对的
0.1的表示方法
0.1对应的二进制 0.000110011001100...(循环)
那么1对应的浮点数应该长成这样
1.10011001100....*2的-4次方
所以尾数位应该是10011001100....
指数应该是1019(1019-1023=-4)也就是01111111011
看来我们是对的
0.5,0.25的表示
0.5
0.25
观察
我们从上面的浮点数二进制码可以看出一个规律,
如果是整数,尾数都是0
如果是小数,如果是0.5 0.25 0.125这种的,尾数都是0
如果是0.1,0.2,0.4这种的小数,尾数都是无限循环的
2.精度产生的原因
为什么精度会产生呢
首先0.1这种转化为二进制码是有误差的,尾数是一个不断循环的数,但是我们浮点数的尾数尾数是有限的,所以需要省略一部分,这就产生了一部分误差
其次,在浮点数加法运算里面,有对阶操作。対阶会损失一部分尾数,如果尾数后面都是0(整数或者0.5之类的),没影响。但是如果是0.1转换成的二进制浮点数码的尾数,対阶的时候舍弃部分尾数明显也会造成误差。
所以可以说误差产生在对尾数的处理中
如果一个大数和一个小数相加时,会产生很大的误差,因为対阶的时候尾数得截掉好多位
(1+0.1).toPrecision(20) //"1.1000000000000000888"
(100000000000+0.1).toPrecision(20)
"100000000000.10000610"
很明显误差大了
3.关于精度的额外知识点
0.1并不是0.1
上面的内容你如果理解了,你再看到0.1,你就会清楚,0.1并不是0.1,0.1的浮点数二进制码是有误差的,不可能算出0.1
我们看到的是浏览器帮我们做了处理的
不信,你可以试试
0.1.toPrecision(17)
// "0.10000000000000001"
JS安全数
面试喜欢考这个问题,啥叫安全数,就是在这个范围数值都有一正一反,一一对应
尾数一共52位,加上一个隐藏位,共53位
也就是JS能表示的最大整数是2的53次方,这个数是16位
但是
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true
实际上2的53次方都不安全了,所以是2的53次方-1
在es6中 是Number.MAX_SAFE_INTEGER
toPrecision vs toFixed
数据处理时,这两个函数很容易混淆。它们的共同点是把数字转成字符串供展示使用。注意在计算的中间过程不要使用,只用于最终结果。
不同点就需要注意一下:
toPrecision
是处理精度,精度是从左至右第一个不为0的数开始数起。toFixed
是小数点后指定位数取整,从小数点开始数起。
两者都能对多余数字做凑整处理,也有些人用toFixed
来做四舍五入,但一定要知道它是有 Bug 的。
如:1.005.toFixed(2)
返回的是1.00
而不是1.01
。
原因:1.005
实际对应的数字是1.00499999999999989
,在四舍五入时全部被舍去!
不过(1.005).toPrecision(4) ===> 1.005
怎么解决这个问题呢,自己写一套字符串逻辑去处理也可以
if (!Number.prototype._toFixed) {
Number.prototype._toFixed = Number.prototype.toFixed;
}
Number.prototype.toFixed = function(n) {
return (this + 3e-16)._toFixed(n);
};
//加上一个非常非常小的数,此时就正常了
1.005.toFixed(2)
"1.01"
4.解决误差方案
误差主要产生在进制转化和浮点数运算的対阶操作中
整数由于尾数后面全是0,同时转化为二进制数没有误差,所以
我们第一种方案就是全部转化为整数,计算完再转化为小数类似这种
/**
* 精确加法
*/
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
我们第二种方案就是用现成的库 mathjs之类的,原理就是不走浮点数那一套,转化成字符串,自己实现运算逻辑,从性能上说肯定比原生慢一点
有小数运算就是有风险的 比如0.56*100 => 56.00000000000001
不过就算不精准也比这种展示好,所以小数计算的结果注意tofixed一下
5.参考资料
理解有限,如果有不对的地方,欢迎大家在评论区指出
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。