Brief                              

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

1. 著名的 0.1 + 0.2 === 0.30000000000000004

2. 1000000000000000128 === 1000000000000000129

IEEE 754 Floating-point                  

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

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

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

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

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

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

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

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

基础野:细说原码、反码和补码(http://www.cnblogs.com/fsjohnhuang/p/5060242.html)

基础野:细说无符号整数(http://www.cnblogs.com/fsjohnhuang/p/5078290.html)

基础野:细说有符号整数(http://www.cnblogs.com/fsjohnhuang/p/5082829.html)

基础野:细说浮点数(http://www.cnblogs.com/fsjohnhuang/p/5109766.html)

Why 0.1 + 0.2 === 0.30000000000000004 ?       

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

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

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

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

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

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

Why 0.7 * 180===125.99999999998 ?          

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

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

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

Why 1000000000000000128 === 1000000000000000129 ?      

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

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

3. 因此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(".")[].length;
} catch (e) {
baseNum1 = ;
}
try {
baseNum2 = num2.split(".")[].length;
} catch (e) {
baseNum2 = ;
}
baseNum = Math.pow(, Math.max(baseNum1, baseNum2));
return (num1 * baseNum + num2 * baseNum) / baseNum;
};

Solution 0x01 - math.js

若需要复杂且全面的运算功能那必须上math.js,其内部引用了decimal.jsfraction.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(-, )
console.log(quotient + '') // -(1+1/2)

解题思路:

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

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

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

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

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

4.2. 按位拆分:假设以3个数值为一组对12345进行拆分得到345和12,而实际值为12*1000 + 345。

就我而言,4.1的拆分规则结构不稳定,而且不直观;而4.2的规则直观,且拆分和恢复的公式固定。

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

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

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

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

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

3. 基于上述的数值编码规则如何实现其它如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

JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后的更多相关文章

  1. JS魔法堂:LINK元素深入详解

    一.前言 我们一般使用方式为 <link type="text/css" rel="stylesheet" href="text.css&quo ...

  2. JS魔法堂:jsDeferred源码剖析

    一.前言 最近在研究Promises/A+规范及实现,而Promise/A+规范的制定则很大程度地参考了由日本geek cho45发起的jsDeferred项目(<JavaScript框架设计& ...

  3. JS魔法堂:属性、特性,傻傻分不清楚

    一.前言 或许你和我一样都曾经被下面的代码所困扰 var el = document.getElementById('dummy'); el.hello = "test"; con ...

  4. JS魔法堂:浏览器模式和文档模式怎么玩?

    一.前言 从IE8开始引入了文档兼容模式的概念,作为开发人员的我们可以在开发人员工具中通过“浏览器模式”和“文档模式”(IE11开始改为“浏览器模式”改成更贴切的“用户代理字符串”)品味一番,它的出现 ...

  5. JS魔法堂:不完全国际化&本地化手册 之 实战篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

  6. JS魔法堂:判断节点位置关系

    一.前言 在polyfill querySelectorAll 和写弹出窗时都需要判断两个节点间的位置关系,通过jQuery我们可以轻松搞定,但原生JS呢?下面我将整理各种判断方法,以供日后查阅. 二 ...

  7. JS魔法堂:IMG元素加载行为详解

    一.前言 在<JS魔法堂:jsDeferred源码剖析>中我们了解到img元素加载失败可以作为函数异步执行的优化方案,本文打算对img元素的加载行为进行更深入的探讨. 二.资源加载的相关属 ...

  8. JS魔法堂:那些困扰你的DOM集合类型

    一.前言 大家先看看下面的js,猜猜结果会怎样吧! 可选答案: ①. 获取id属性值为id的节点元素 ②. 抛namedItem is undefined的异常 var nodes = documen ...

  9. JS魔法堂:doctype我们应该了解的基础知识

    一.前言 什么是doctype?其实我们一直使用,却很少停下来看清楚它到底是什么,对网页有什么作用.本篇将和大家一起探讨那个默默无闻的doctype吧! 二.什么是doctype doctype或DT ...

  10. JS魔法堂:追忆那些原始的选择器

    一.前言                                                                                                 ...

随机推荐

  1. centos install kafka and zookeeper

    1.安装zookeeper ZooKeeper is a distributed, open-source coordination service for distributed applicati ...

  2. Java虚拟机7:内存分配原则

    前言 对象的内存分配,往大的方向上讲,就是在堆上分配,少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节决定于当前使用的是哪种垃圾收集器组合,当然还有虚拟机中与内存相关的参数 ...

  3. 防止开发人员获取到敏感数据(SQL Server的数据加密简介)

    背景 有时候,我们还真的会碰到这样的需求:防止开发人员获取到敏感数据.也许你觉得很简单,把开发和运营分开不就可以了吗?是的,如果公司有专门的运营团队的话,但对于很多小公司来说,几个人的开发团队就兼顾了 ...

  4. 基础调试命令 - .dump/.dumpcap/.writemem/!runaway

    Windbg是windows平台上强大的调试器,它相对于其他常见的IDE集成的调试器有几个重要的优势, Windbg可以做内核态调试 Windbg可以脱离源代码进行调试 Windbg可以用来分析dum ...

  5. Nunit工具做C#的单元测试

      Nunit工具做C#的单元测试 学习心得 编写人:罗旭成 时间:2013年9月2日星期一 1.开发人员如何做单元测试 单元测试是针对最小的可测试软件元素(单元)的,它所测试的内容包括单元的内部结构 ...

  6. windows 8.1 试用感受:蛋疼感大幅降低

    众所周知windows 8 的最大使用感受就是蛋疼. 无论是微软MVP,还是我这样的万年不悔微软小白鼠,普通用户,小白用户,或多或少的都对这款操作系统感到蛋疼. 槽点太多,以至于大家都懒得批判了.好在 ...

  7. Ubuntu下解决adb devices:???????????? no permissions的方法

    之前一直都是在windows下进行开发的,但是这次由于一个小模块用的东西只能在linux下运行,所以就发生了这么一系列的问题环境:虚拟机Vmware下的Ubuntu12.10事件:连接android手 ...

  8. 在github上写个人简历——最简单却又不容易的内容罗列

    前篇博客分享了一下自己初入github的过程,傻瓜式一步步搭建好主页后,终于该做正事儿了——写简历.在脑袋中构思了很多版本,最后终于决定,先写一个最传统的版本,于是我在箱子中翻出我word版本的简历, ...

  9. AngularJS快速入门指南12:模块

    AngularJS模块定义了一个application. 模块是一个application中不同部分的容器. application中的所有控制器都应该属于一个模块. 带有一个控制器的模块 下面这个a ...

  10. 一则线上MySql连接异常的排查过程

    Mysql作为一个常用数据库,在互联网系统应用很多.有些故障是其自身的bug,有些则不是,这里以前段时间遇到的问题举例. 问题 当时遇到的症状是这样的,我们的应用在线上测试环境,JMeter测试过程中 ...