当前位置: 首页 > 科技观察

JavaScript浮点陷阱及解决方法

时间:2023-03-12 01:57:08 科技观察

众所周知,JavaScript浮点计算经常会遇到0.000000001和0.999999999这样的奇怪结果,比如0.1+0.2=0.300000000000000004,1-0.9=0.0999999999999999998,很多人都知道这是一个浮点错误问题,但具体不清楚。这篇文章会帮你理清这背后的原理和解决方法,也会给你讲解JS中的大数危机和四次算术运算遇到的坑。浮点数的存储首先要搞清楚JavaScript是如何存储小数的。与Java和Python等其他语言不同,JavaScript中的所有数字,包括整数和小数,都只有一种类型——数字。其实现遵循IEEE754标准,采用64位定长表示,即标准的double双精度浮点数(也有32位单精度float)。在计算机组成原理中已经详细介绍过了,不记得也没关系。这样的存储结构的好处是可以对整数和小数进行归一化,节省存储空间。64位的位可以分为三部分:符号位S:第一位是正负符号位(sign),0代表正数,1代表负数指数位E:中间11位存放指数,用于表示幂数的尾数位M:***的52位为尾数(mantissa),超出部分自动四舍五入为零。实际数可以通过以下公式计算:?$V=(-1)^{S}\timesM\times2^{E}$注意上面的公式是科学记数法,十进制是0M=001。E为无符号整数,因为长度为11位,取值范围为0~2047。但是科学计数法中的指数可以是负数,所以减去一个中间数1023,[0,1022]是负数,[1024,2047]是正数。例如4.5的指数E=1025,尾数M为001。Thefinalformulabecomes:$V=(-1)^{S}\times(M+1)\times2^{E-1023}$So4.5isfinallyexpressedas(M=001,E=1025):?(Thepictureisgeneratedfromthishttp://www.binaryconvert.com/convert_double.html)Let’stake0.1asanexampletoexplainthereasonforthefloatingpointerror.0.1convertedintobinaryisrepresentedas0.0001100110011001100(1100cycle),1.100110011001100x2^-4,soE=-4+1023=1019;Mdiscardsthefirst1toget100110011....Intheend,itis:afterconvertingtodecimal,itis0.100000000000000005551115123126,sothereisafloatingpointerror.为什么0.1+0.2=0.30000000000000004?计算步骤为://0.1和0.2都转化成二进制后再进行运算0.00011001100110011001100110011001100110011001100110011010+0.0011001100110011001100110011001100110011001100110011010=0.0100110011001100110011001100110011001100110011001100111//转成十进制正好是0.30000000000000004为什么x=0.1能得到0.1?恭喜你到了看山Nottherealmofmountains.Becausethefixedlengthofmantissais52bits,plustheomittedonebit,themaximumnumberthatcanberepresentedis2^53=9007199254740992,andthecorrespondingscientificnotationmantissais9.007199254740992,whichisalsothemaximumprecisionthatJScanrepresent.Itslengthis16,soyoucanusetoPrecision(16)todoprecisioncalculations,andtheexcessprecisionwillbeautomaticallyroundedup.Sothereis:0.100000000000000000555.toPrecision(16)//Returns0.1000000000000000,whichisexactly0.1afterremovingthezeroattheend//Butthe`0.1`youseeisnotactually`0.1`.不信你可以试试更高的精度:0.1.toPrecision(21)=0.100000000000000005551大数危机你可能已经隐约感觉到了。如果整数大于9007199254740992会发生什么?由于M***值为1023,所以***可以表示的整数是2^1024-1。这是可以表示的最大整数。但是你不能这样计算这个数字,因为从2^1024变成Infinity>Math.pow(2,1023)8.98846567431158e+307>Math.pow(2,1024)Infinity然后for(2^53,2^63)之间的数字会怎样?(2^53,2^54)会二选一,只能准确表示偶数(2^54,2^55)会二选一,只能准确表示4的倍数...依次跳过更多2的倍数。下图可以很好的表示JavaScript中浮点数和实数(RealNumbers)的对应关系。我们常用的(-2^53,2^53)只是中间很小的一部分,越往两侧越稀疏不精确。在淘宝早期的订单系统中,订单号被当作一个数字来处理。后来随机订单号猛增,已经超过了9007199254740992,最后的解决办法是把订单号改成字符串。解决大数问题可以参考第三方库bignumber.js。原理是把所有数字都当成字符串,重新实现计算逻辑。缺点是性能比原来差很多。因此,有必要原生支持大数。现在TC39已经有了Stage3proposalbigint,彻底解决了大数的问题。toPrecisionvstoFixed数据处理时,这两个函数很容易混淆。它们的共同点是将数字转换为字符串进行显示。注意不要在计算中间使用它,只用于最终结果。需要注意区别:toPrecision是处理精度,精度从左到右第一个不为0的数开始计算。toFixed是将小数点后指定位数四舍五入,从小数点开始计算。两者都可以对多余的数字进行四舍五入,也有人用toFixed进行四舍五入,但是一定要知道它有bug。例如:1.005.toFixed(2)返回1.00而不是1.01。原因:1.005对应的实际数为1.00499999999999989,四舍五入时全部四舍五入!解决方法:使用专业的舍入函数Math.round()进行处理。但是Math.round(1.005*100)/100仍然不起作用,因为1.005*100=100.49999999999999。使用Math.round前还需要解决乘除法精度误差。可以使用后面介绍的number-precision#round方法来解决。解决方案回到最关心的问题:如何解决浮点数错误。首先,理论上,用有限的空间来存储最大的小数是无法保证准确性的,但是我们可以对其进行处理,得到我们期望的结果。数据展示类获取1.40000000000000001这样的数据展示时,建议先用toPrecision取整,用parseFloat转成数字再展示,如下:parseFloat(1.4000000000000001.toPrecision(12))===1.4//True封装成一个方法就是:functionstrip(num,precision=12){return+parseFloat(num.toPrecision(precision));}为什么选择12作为默认精度呢?这是一个经验选择。一般选择12可以解决大部分0001和0009的问题,而且大多数情况下已经足够了,如果需要更高的精度可以增加。数据操作类对于+-*/等操作类型的操作,不能使用toPrecision。正确的方法是先把小数化成整数再计算。以加法为例:/***精确加法*/functionadd(num1,num2){constnum1Digits=(num1.toString().split('.')[1]||'').length;constnum2Digits=(num2.toString().split('.')[1]||'').length;constbaseNum=Math.pow(10,Math.max(num1Digits,num2Digits));return(num1*baseNum+num2*baseNum)/baseNum;}以上方法可以适用于大部分场景。当遇到科学计数法如2.3e+1(当数字的精度大于21时,数字会被强制以科学计数法的形式显示),需要特殊处理。能看懂这个,说明你很有耐心,所以我给你加个bonus。遇到浮点数错误问题,可以直接使用https://github.com/dt-fe/number-precision***支持浮点数的加减乘除四舍五入等运算.非常小,只有1K,远小于大多数同类库(如Math.js、BigDecimal.js),100%全测试覆盖,代码可读性强,不妨在你的应用中使用!参考WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic为什么计算机不擅长代数|无限系列您的模型容易出现浮点错误吗?IEEE754