资讯专栏INFORMATION COLUMN

JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后

JerryWangSAP / 610人阅读

摘要:也就是说不仅是会产生这种问题,只要是采用的浮点数编码方式来表示浮点数时,则会产生这类问题。到这里我们都理解只要采取的浮点数编码的语言均会出现上述问题,只是它们的标准类库已经为我们提供了解决方案而已。

Brief

一天有个朋友问我“JS中计算0.7 * 180怎么会等于125.99999999998,坑也太多了吧!”那时我猜测是二进制表示数值时发生round-off error所导致,但并不清楚具体是如何导致,并且有什么方法去规避。于是用了3周时间静下心把这个问题搞懂,在学习的过程中还发现不仅0.7 * 180==125.99999999998,还有以下的坑

著名的 0.1 + 0.2 === 0.30000000000000004

1000000000000000128 === 1000000000000000129

IEEE 754 Floating-point

众所周知JS仅有Number这个数值类型,而Number采用的时IEEE 754 64位双精度浮点数编码。而浮点数表示方式具有以下特点:

浮点数可表示的值范围比同等位数的整数表示方式的值范围要大得多;

浮点数无法精确表示其值范围内的所有数值,而有符号和无符号整数则是精确表示其值范围内的每个数值;

浮点数只能精确表示m*2e的数值;

当biased-exponent为2e-1-1时,浮点数能精确表示该范围内的各整数值;

当biased-exponent不为2e-1-1时,浮点数不能精确表示该范围内的各整数值。

由于部分数值无法精确表示(存储),于是在运算统计后偏差会愈见明显。

想了解更多浮点数的知识可参考以下文章:

《基础野:细说原码、反码和补码》

《基础野:细说无符号整数》

《基础野:细说有符号整数》

《基础野:细说浮点数》

Why 0.1 + 0.2 === 0.30000000000000004?

在浮点数运算中产生误差值的示例中,最出名应该是0.1 + 0.2 === 0.30000000000000004了,到底有多有名?看看这个网站就知道了http://0.30000000000000004.com/。也就是说不仅是JavaScript会产生这种问题,只要是采用IEEE 754 Floating-point的浮点数编码方式来表示浮点数时,则会产生这类问题。下面我们来分析整个运算过程。

0.1 的二进制表示为 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-4;

当64bit的存储空间无法存储完整的无限循环小数,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.1实际存储时的位模式是0-01111111011-1001100110011001100110011001100110011001100110011010;

0.2 的二进制表示为 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-3;

当64bit的存储空间无法存储完整的无限循环小数,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.2实际存储时的位模式是0-01111111100-1001100110011001100110011001100110011001100110011010;

实际存储的位模式作为操作数进行浮点数加法,得到 0-01111111101-0011001100110011001100110011001100110011001100110100。转换为十进制即为0.30000000000000004。

Why 0.7 * 180===125.99999999998?

0.7实际存储时的位模式是0-01111111110-0110011001100110011001100110011001100110011001100110;

180实际存储时的位模式是0-10000000110-0110100000000000000000000000000000000000000000000000;

实际存储的位模式作为操作数进行浮点数乘法,得到0-10000000101-1111011111111111111111111111111111111111101010000001。转换为十进制即为125.99999999998。

Why 1000000000000000128 === 1000000000000000129?

1000000000000000128实际存储时的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;

1000000000000000129实际存储时的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;

因此1000000000000000128和1000000000000000129的实际存储的位模式是一样的。

Solution

到这里我们都理解只要采取IEEE 754 FP的浮点数编码的语言均会出现上述问题,只是它们的标准类库已经为我们提供了解决方案而已。而JS呢?显然没有。坏处自然是掉坑了,而好处恰恰也是掉坑了:)

针对不同的应用需求,我们有不同的实现方式。

Solution 0x00 - Simple implementation

对于小数和小整数的简单运算可用如下方式

function numAdd(num1/*:String*/, num2/*:String*/) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) { 
        baseNum1 = 0; 
    } 
    try { 
        baseNum2 = num2.split(".")[1].length; 
    } catch (e) { 
        baseNum2 = 0;
    } 
    baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); 
    return (num1 * baseNum + num2 * baseNum) / baseNum; 
};
Solution 0x01 - math.js

若需要复杂且全面的运算功能那必须上math.js,其内部引用了decimal.js和fraction.js。功能异常强大,用于生产环境上妥妥的!

Solution 0x02 - D.js

D.js算是我的练手项目吧,截止本文发表时D.js版本为V0.2.0,仅实现了加、减、乘和整除运算而已,bug是一堆堆的,但至少解决了0.1+0.2的问题了。

var sum = D.add(0.1, 0.2)
console.log(sum + "") // 0.3

var product = D.mul("1e-2", "2e-4")
console.log(product + "") // 0.000002

var quotient = D.div(-3, 2)
console.log(quotient + "") // -(1+1/2)

解题思路:

由于仅位于Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER间的整数才能被精准地表示,也就是只要保证运算过程的操作数和结果均落在这个阀值内,那么运算结果就是精准无误的;

问题的关键落在如何将小数和极大数转换或拆分为Number.MIN_SAFE_INTEGER至Number.MAX_SAFE_INTEGER阀值间的数了;

小数转换为整数,自然就是通过科学计数法表示,并通过右移小数点,减小幂的方式处理;(如0.000123 等价于 123 * 10-6)

而极大数则需要拆分,拆分的规则是多样的。

按因式拆分:假设对12345进行拆分得到 5 * 2469;

按位拆分:假设以3个数值为一组对12345进行拆分得到345和12,而实际值为12*1000 + 345。
就我而言,1 的拆分规则结构不稳定,而且不直观;而 2 的规则直观,且拆分和恢复的公式固定。

余数由符号位、分子和分母组成,而符号与整数部分一致,因此只需考虑如何表示分子和分母即可。

无限循环数则仅需考虑如何表示循环数段即可。(如10.2343434则分成10.23 和循环数34和34的权重即可)

得到编码规则后,那就剩下基于指定编码如何实现各种运算的问题了。

基于上述的数值编码规则如何实现加、减运算呢?

基于上述的数值编码规则如何实现乘、除运算呢?(其实只要加、减运算解决了,乘除必然可解,就是效率问题而已)

基于上述的数值编码规则如何实现其它如sin、tan、%等数学运算呢?

另外由于涉及数学运算,那么将作为add、sub、mul和div等入参的变量保持如同数学公式运算数般纯净(Persistent/Immutable Data Structure)是必须的,那是否还要引入immutable.js呢?(D.js现在采用按需生成副本的方式,可预见随着代码量的增加,这种方式会导致整体代码无法维护)

Conclusion

依照我的尿性,D.js将采取不定期持续更新的策略(待我理解Persistent/Immutable Data Structure后吧:))。欢迎各位指教!

尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/5115672.html ^_^肥子John

Thanks

http://es5.github.io
https://github.com/MikeMcl/decimal.js/
http://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html
http://demon.tw/copy-paste/javascript-precision.html

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79328.html

相关文章

  • 0.1 + 0.2 != 0.3背后原理

    摘要:标准是浮点数算术标准的标准编号,等同于国际标准。标准规定了计算机程序设计环境中的二进制和十进制的浮点数之间的交换算术格式以及方法。 初学JavaScript,在进行小数(浮点数)运算时,经常会碰到这样的情况:0.1 + 0.2=0.30000000000000004,记得当时,教程告诉我们说,0.1 + 0.2在JavaScript运算中,它的值是不固定的,可以在后面学习和试验中,渐渐...

    阿罗 评论0 收藏0
  • 如何解决0.1 +0.2===0.30000000000000004类问题

    摘要:方法使用定点表示法来格式化一个数,会对结果进行四舍五入。该数值在必要时进行四舍五入,另外在必要时会用来填充小数部分,以便小数部分有指定的位数。如果数值大于,该方法会简单调用并返回一个指数记数法格式的字符串。在环境中,只能是之间,测试版本为。 showImg(https://segmentfault.com/img/remote/1460000011913134?w=768&h=521)...

    yuanzhanghu 评论0 收藏0
  • 深度剖析0.1 +0.2===0.30000000000000004原因

    摘要:吐槽一句,大二的专业课数字逻辑电路终于用在工作上了。,整数位为,且精度只到十分位,因此是。如果是不限精度的话,转换后的二进制数应该是无限循环。再看一下百科给出的标准因此,的类型,最高的位是符号位,接着的位是指数,剩下的位为有效数字。 showImg(https://segmentfault.com/img/remote/1460000011902479?w=600&h=600); 用一...

    haobowd 评论0 收藏0
  • CSS魔法:你真理解z-index吗?

    摘要:与的映射关系为。与根对应的对应的层叠上下文,是其他的祖先,的范围覆盖整条。注意的默认值为,自动赋值为。对于,它会将赋予给对应的,而则不会。 一、前言                                假如只是开发简单的弹窗效果,懂得通过z-index来调整元素间的层叠关系就够了。但要将多个弹窗间层叠关系给处理好,那么充分理解z-index背后的原理及兼容性问题就是必要的知识...

    andycall 评论0 收藏0
  • 探寻 JavaScript 精度问题以及解决方案

    摘要:推导为何等于在中所有数值都以标准的双精度浮点数进行存储的。先来了解下标准下的双精度浮点数。精度位总共是,因为用科学计数法表示,所以首位固定的就没有占用空间。验证完成的最大安全数是如何来的根据双精度浮点数的构成,精度位数是。 阅读完本文可以了解到 0.1 + 0.2 为什么等于 0.30000000000000004 以及 JavaScript 中最大安全数是如何来的。 十进制小数转为二...

    YanceyOfficial 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<