-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JavaScript 浮点数陷阱及解法 #9
Comments
@camsong 感谢作者的分享,文章很不错 👍 其中 大数字危机 一节中:
这句有点困惑,指数位最大值为 2047,减去 1023 后应该是 1024 吧,所以最大能表示的数为 2^1024 - 1 ? JavaScript能表示并进行精确算术运算的整数范围为:正负2的53次方;超过范围的,无法给出精确计算结果,您文章给出的配图: JavaScript 中浮点数和实数(Real Number)之间的对应关系 也解释了这一点。
这个段代码也验证了:(2^53, 2^54) 之间的数会两个选一个,只能精确表示偶数。而这一点应该也可以作为回答知乎问题的理由之一吧:javascript 里最大的安全的整数为什么是2的53次方减一? |
@YingshanDeng typo fixed。能解释,我也正是想说明这个问题。 |
666,感谢分享干货,已推荐到 SegmentFault 头条 (๑•̀ㅂ•́)و✧ |
@natee 本来就加了,只是 Github 不支持 Latex,已换成截图 |
感觉只要strip转换一下最终的计算结果,计算就正确了。是不是不需要精确的加减乘除 |
@shoung6 |
嗯嗯,那什么情况是strip实现不了,必须用精确加减乘除的吗?我感觉我能想到的计算需求,只要计算完成之后strip一下就正确了,就不需要加减乘除那几个函数了~ |
@shoung6 外部传入的“异常”数据需要展现的时候。如后端接口返回 |
这个是用 |
文中 |
嗯嗯,谢谢解答~ |
想请教个问题: |
还有个问题: |
@mmmmmaster 我来回答一下这两个问题吧 😋 ②首先我们要知道
所以我们看到 (2^53, 2^54) 范围的数字,都是间隔 2 的。 然后我们还要了解到不是 safe integers 的数字,计算结果不能确保其正确性。所以你提到的那几个计算中有些正确,有些不正确。 第二个问题关键在于不要混淆这两个概念即可。 最后,@camsong 这么理解对吧 😂 |
@YingshanDeng |
@mmmmmaster 根据我的理解,来解释一下你提到的 0.1 “误差偏大”问题 😀 |
@YingshanDeng ,还真是,之前没注意有进位,多谢! |
终于把我关于 |
赞 |
大神,省略的一位是什么意思呢 |
非0数用十进制的科学计数法表示时首位为 |
科学计数法的话,10进制的M应该是1=<M<10吧。 |
对整数为言,1=<M<10 与文中的 0<M<10 对等。 |
有一个疑问,为何C语言中,同样64位双精度的0.1 + 0.2 能计算到结果呢?不知博主是否知道 double a = 0.1;
double b = 0.2;
printf("%lf",a+b); // 0.3 |
@WuHuaJi0 你printf的时候指定了输出格式,所以会截取。 |
@FullStack1994 toPrecision 的返回值是字符串类型。 |
2 ** 53应该是开区间吧,不包括 2 ** 53,应该是(-2 ** 53, 2 ** 53), 尾数表精度,这也是JS里面最大安全整数 |
👇最大安全整数,IEEE 754标准一共有53位的尾数(包含省略的1位),类似于科学计数法,尾数表示的是精度,一个数对应一个IEEE 754的双精度浮点数,所以是安全的,当多个数对应一个浮点数的时候就是不安全的 👇最大数,根据IEEE 754标准的定义来的。为什么指数减去52,因为尾数表示的是1.1111(52位),尾数左移指数减去对应的位数,所以这个就是最大值 |
为什么 E 最大值是 1023?E 的取值是 [0,2047],然后减去中间数 1023, 那最大值不是 1024 吗 |
2047被作为特殊类型处理, 即NaN, Infinity, -Infinity |
|
这个2^1024 - 1是最大整数是怎么计算来的,理论上2^970 * (2^54 - 1) 就已经是Infinity了 |
👍 |
看完这篇blog后写了一篇文章,感兴趣的可以看看 前端应该知道的JavaScript浮点数和大数的原理 |
有一个疑惑,尾数M会先省略掉前面的1才存储到52位里面。那么1和0在存储的时候是怎么区分开来的呢? |
您好,您的邮件我已收到,会尽快给您答复!
|
这样为什么一开始设计的时候不把尾数位划大一点,指数位划小几位呢? |
您好,您的邮件我已收到,会尽快给您答复!
|
您好,您的邮件我已收到,会尽快给您答复!
|
众所周知,JavaScript 浮点数运算时经常遇到会
0.000000001
和0.999999999
这样奇怪的结果,如0.1+0.2=0.30000000000000004
、1-0.9=0.09999999999999998
,很多人知道这是浮点数误差问题,但具体就说不清楚了。本文帮你理清这背后的原理以及解决方案,还会向你解释JS中的大数危机和四则运算中会遇到的坑。浮点数的存储
首先要搞清楚 JavaScript 如何存储小数。和其它语言如 Java 和 Python 不同,JavaScript 中所有数字包括整数和小数都只有一种类型 —
Number
。它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。计算机组成原理中有过详细介绍,如果你不记得也没关系。这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。
64位比特又可分为三个部分:

实际数字就可以用以下公式来计算:

注意以上的公式遵循科学计数法的规范,在十进制是为0<M<10,到二进行就是0<M<2。也就是说整数部分只能是1,所以可以被舍去,只保留后面的小数部分。如 4.5 转换成二进制就是 100.1,科学计数法表示是 1.001*2^2,舍去1后
M = 001
。E是一个无符号整数,因为长度是11位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以再减去一个中间数 1023,[0,1022]表示为负,[1024,2047] 表示为正。如4.5 的指数E = 1025
,尾数M为 001。最终的公式变成:
所以
4.5
最终表示为(M=001、E=1025):
(图片由此生成 http://www.binaryconvert.com/convert_double.html)
下面再以
0.1
例解释浮点误差的原因,0.1
转成二进制表示为0.0001100110011001100
(1100循环),1.100110011001100x2^-4
,所以E=-4+1023=1019
;M 舍去首位的1,得到100110011...
。最终就是:
转化成十进制后为
0.100000000000000005551115123126
,因此就出现了浮点误差。为什么
0.1+0.2=0.30000000000000004
?计算步骤为:
为什么
x=0.1
能得到0.1
?恭喜你到了看山不是山的境界。因为 mantissa 固定长度是 52 位,再加上省略的一位,最多可以表示的数是
2^53=9007199254740992
,对应科学计数尾数是9.007199254740992
,这也是 JS 最多能表示的精度。它的长度是 16,所以可以使用toPrecision(16)
来做精度运算,超过的精度会自动做凑整处理。于是就有:大数危机
可能你已经隐约感觉到了,如果整数大于 9007199254740992 会出现什么情况呢?
由于 E 最大值是 1023,所以最大可以表示的整数是
2^1024 - 1
,这就是能表示的最大整数。但你并不能这样计算这个数字,因为从2^1024
开始就变成了Infinity
那么对于
(2^53, 2^63)
之间的数会出现什么情况呢?(2^53, 2^54)
之间的数会两个选一个,只能精确表示偶数(2^54, 2^55)
之间的数会四个选一个,只能精确表示4个倍数下面这张图能很好的表示 JavaScript 中浮点数和实数(Real Number)之间的对应关系。我们常用的
(-2^53, 2^53)
只是最中间非常小的一部分,越往两边越稀疏越不精确。在淘宝早期的订单系统中把订单号当作数字处理,后来随意订单号暴增,已经超过了
9007199254740992
,最终的解法是把订单号改成字符串处理。要想解决大数的问题你可以引用第三方库 bignumber.js,原理是把所有数字当作字符串,重新实现了计算逻辑,缺点是性能比原生的差很多。所以原生支持大数就很有必要了,现在 TC39 已经有一个 Stage 3 的提案 proposal bigint,大数问题有望彻底解决。在浏览器正式支持前,可以使用 Babel 7.0 来实现,它的内部是自动转换成 big-integer 来计算,要注意的是这样能保持精度但运算效率会降低。
toPrecision
vstoFixed
数据处理时,这两个函数很容易混淆。它们的共同点是把数字转成字符串供展示使用。注意在计算的中间过程不要使用,只用于最终结果。
不同点就需要注意一下:
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.4000000000000001
这样的数据要展示时,建议使用toPrecision
凑整并parseFloat
转成数字后再显示,如下:封装成方法就是:
为什么选择
15
做为默认精度?这是一个经验的选择,一般选15就能解决掉大部分0001和0009问题,而且大部分情况下也够用了,如果你需要更精确可以调高。数据运算类
对于运算类操作,如
+-*/
,就不能使用toPrecision
了。正确的做法是把小数转成整数后再运算。以加法为例:以上方法能适用于大部分场景。遇到科学计数法如
2.3e+1
(当数字精度大于21时,数字会强制转为科学计数法形式显示)时还需要特别处理一下。能读到这里,说明你非常有耐心,那我就放个福利吧。遇到浮点数误差问题时可以直接使用
https://github.com/dt-fe/number-precision
完美支持浮点数的加减乘除、四舍五入等运算。非常小只有1K,远小于绝大多数同类库(如Math.js、BigDecimal.js),100%测试全覆盖,代码可读性强,不妨在你的应用里用起来!
参考
当然写这篇文章是为了招聘!!!
The text was updated successfully, but these errors were encountered: