1. 存储原理:

在计算机中数字无论是定点数还是浮点数都是以多位二进制的方式进行存储的。
事实上不仅仅是 Javascript,在很多语言中 0.1 + 0.2 都会得到 0.30000000000000004,为此还诞生了一个好玩的网站 0.30000000000000004。究其根本,这些语言中的数字都是以IEEE 754 双精度 64 位浮点数 来存储的,它由64位组成,这64位由3部分组成,(S:符号位,Exponent:指数域,Fraction:尾数域)。它的表示格式为:

s 是符号位,表示正负。m 是尾数,有 52 bits。e 是指数,有 11 bits,e 的范围是 [-1074, 971]ECMAScript 5 规范),这样其实很容易推出 Javascript 能表示的最大数为:

1 * (Math.pow(2, 53) - 1) * Math.pow(2, 971) = 1.7976931348623157e+308

而这个数也就是 Number.MAX_VALUE 的值。

同理可推得 Number.MIN_VALUE 的值:

1 * 1 * Math.pow(2, -1074) = 5e-324

需要注意的是,Number.MIN_VALUE 表示的是最小的比零大的数,而不是最小的数,最小的数很显然是 -Number.MAX_VALUE。

可能你已经注意到,当计算 Number.MAX_VALUE 时,(Math.pow(2, 53) - 1) 的结果用二进制表示是 53 个 1,除了 m 表示的 52 个 bits 外,其实最前面的 1 bit 是隐藏位(隐藏位表示的永远是 1),设置隐藏位为的是能表示更大范围的数。(对于隐藏位我也不是很清楚,一说 "当 指数 e 的二进制位全为 0 时,隐藏位为 0,如果不全为 0,则隐藏位为 1,这应该是基于指数表达式的存储方式决定的,隐藏位也就是指数的底数里面的整数部分,尾数 m 则是指数中底数的 fraction 小数部分" 详见 Javascript 中小数和大整数的精度丢失问题

复习了一些组成原理的知识后,我们再回到 0.1 + 0.2 这道题本身。我们都知道,计算机中的数字都是以二进制存储的,如果要计算 0.1 + 0.2 的结果,计算机会先把 0.1 和 0.2 分别转化成二进制,然后相加,最后再把相加得到的结果转为十进制。

我们先把 0.1 和 0.2 分别转化为二进制,十进制转为二进制这里就不多说了,整数部分 "除二取余,倒序排列",小数部分 "乘二取整,顺序排列"。也可以用 Javascript 的 toString(2)方法验证转换的结果。

// 0.1 转化为二进制
0.0 0011 0011 0011 0011...(0011循环) // 0.2 转化为二进制
0.0011 0011 0011 0011 0011...(0011循环)

当然计算机并不能表示无限小数,毕竟只有有限的资源,于是我们得把它们用 IEEE 754 双精度 64 位浮点数 来表示:

e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)

当然,真实的计算机存储中 m 并不会是一个小数,而是上面的小数点后的 52 bits,小数点前的 1 为隐藏位

这里又出现一个问题,虽然我们已经明确 m 只能有 52 位(小数点后),但是如果第 53 位是 1,是该进位还是不进位?这里需要考虑 IEEE 754 Rounding modes,可以看下这篇文章 浮点数解惑,或者听我简单地解释下。

关于默认的舍入规则,简单的说,如果 1.101 要保留一位小数,可能的值是 1.1 和 1.2,那么先看 1.101 和 1.1 或者 1.2 哪个值更接近,毫无疑问是 1.1,于是答案是 1.1。那么如果要保留两位小数呢?很显然要么是 1.10 要么是 1.11,而且又一样近,这时就要看这两个数哪个是偶数(末位是偶数),保留偶数为答案。综上,如果第 52 bit 和 53 bit 都是 1,那么是要进位的。

另外,相加时如果指数不一致,需要对齐,一般情况下是向右移,因为最右边的即使溢出了,损失的精度远远小于左边溢出。

接下去就不难了:

e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
---------------------------------------------------------------------------
e = -3; m = 0.1100110011001100110011001100110011001100110011001101
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010
---------------------------------------------------------------------------
e = -3; m = 10.0110011001100110011001100110011001100110011001100111
---------------------------------------------------------------------------
e = -2; m = 1.0011001100110011001100110011001100110011001100110100(52位)
---------------------------------------------------------------------------
= 0.010011001100110011001100110011001100110011001100110100
= 0.30000000000000004(十进制)

而 9007199254740992 + 1 = 9007199254740992 的推理过程大同小异。

9007199254740992 其实就是 2 ^ 53。

e = 0; m = 100000000000000000000000000000000000000000000000000000 (53个0)
+ e = 0; m = 1
---------------------------------------------------------------------------
e = 0; m = 100000000000000000000000000000000000000000000000000001

因为 m 只能有 52 位,而上面相加两数相加后 m 有 53 位(已经除去首位隐藏位),又因为 Rounding modes 的偶数原则,所以将 53 bit 的 1 舍去,所以大小跟 2 ^ 52 并没有变化,试想下,如果是 + 2,那么结果就不一样了。(ps:其实 2^53 在计算机存储中的 m 只能有 52 位,即只有 52 个 0)

事实上,当结果大于 Math.pow(2, 53) 时,会出现精度丢失,导致最终结果存在偏差,而当结果大于 Number.MAX_VALUE,直接返回 Infinity。

2. 解决办法:

1. 只适合小数运算(有bug):

// 解决四维运算,js计算失去精度的问题 

//加法
Number.prototype.add = function(arg){
var r1,r2,m;
try{r1=this.toString().split(".")[1].length}catch(e){r1=0}
try{r2=arg.toString().split(".")[1].length}catch(e){r2=0}
m=Math.pow(10,Math.max(r1,r2))
return (this*m+arg*m)/m
}
//减法
Number.prototype.sub = function (arg){
return this.add(-arg);
}
//乘法
Number.prototype.mul = function (arg)
{
var m=0,s1=this.toString(),s2=arg.toString();
try{m+=s1.split(".")[1].length}catch(e){}
try{m+=s2.split(".")[1].length}catch(e){}
return Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m)
}
//除法
Number.prototype.div = function (arg){
var t1=0,t2=0,r1,r2;
try{t1=this.toString().split(".")[1].length}catch(e){}
try{t2=arg.toString().split(".")[1].length}catch(e){}
with(Math){
r1=Number(this.toString().replace(".",""))
r2=Number(arg.toString().replace(".",""))
return (r1/r2)*pow(10,t2-t1);
}
}
0.1+0.2  // 0.30000000000000004
0.1.add(0.2) //0.3
0.3-0.1 //0.19999999999999998
0.3.sub(0.1) //0.2

2. 引入一些现成的库,比如 math.js 或者 bigNumber.jsdecimal.jsbig.js 等;

js中 0.1+0.2 !== 0.3的更多相关文章

  1. 在js中做数字字符串加0补位,效率分析

    分类: Jquery/YUI/ExtJs 2010-08-30 11:27 2700人阅读 评论(0) 收藏 举报 functiondate算法语言c 通常遇到的一个问题是日期的“1976-02-03 ...

  2. 在js中做数字字符串补0

    转自(http://blog.csdn.net/aimingoo/article/details/4492592) 通常遇到的一个问题是日期的“1976-02-03 HH:mm:ss”这种格式 ,我的 ...

  3. js中setTimeout() 时间参数为0

    当看到下面 这种setTimeout 设置为0 写法的时候一脸懵逼,完全没用过. var fuc = [1,2,3]; for(var i in fuc){ setTimeout(function() ...

  4. java、js中实现无限层级的树形结构(类似递归)

    js中: var zNodes=[ {id:0,pId:-1,name:"Aaaa"}, {id:1,pId:0,name:"A"}, {id:11,pId:1 ...

  5. js中要声明变量吗?

    你好,js语言是弱类型语言,无需申明即可直接使用,默认是作为全局变量使用的.建议:在function里时应使用var 申明变量,这样改变量仅仅只在function的生存周期内存在,不会污染到,全局控件 ...

  6. 在JS中关于堆与栈的认识function abc(a){ a=100; } function abc2(arr){ arr[0]=0; }

    平常我们的印象中堆与栈就是两种数据结构,栈就是先进后出:堆就是先进先出.下面我就常见的例子做分析: main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main( ...

  7. 字符串0.在php和js中转换为布尔类型 值是false还是true

    在php 中 $a = '0'; $b = (bool)$a; var_dump($a);//输出false 在js中官方说明: Note:If the value parameter is omit ...

  8. js中的0就是false,非0就是true及案例

    在处理js代码判断真假时经常会这么写. 但fun()可能得到的是数字0,这可不是表示的没有值,但是!js中的数字0就是false,非0就是true. 于是0就被无情的当做false了. 已经被这个坑过 ...

  9. 深入理解 Node.js 中 EventEmitter源码分析(3.0.0版本)

    events模块对外提供了一个 EventEmitter 对象,即:events.EventEmitter. EventEmitter 是NodeJS的核心模块events中的类,用于对NodeJS中 ...

  10. 为什么js中要用void 0 代替undefined

    这个是Backbone.js中的一句源码 if (callback !== void 0 && 'context' in opts && opts.context == ...

随机推荐

  1. 机器学习-EM算法-GMM模型笔记

    GMM即高斯混合模型,下面根据EM模型从理论公式推导GMM: 随机变量X是有K个高斯分布混合而成,取各个高斯分布的概率为φ1,φ2,... ,φK,第i个高斯分布的均值为μi,方差为Σi.若观测到随机 ...

  2. 海思SDK 与各芯片对照

    本文链接:https://blog.csdn.net/liuxizhen2009/article/details/84261221 海思SDK不对外开放. 海思35系列sdk Hi3507   Hi3 ...

  3. Linux下磁盘分区,格式化以及挂载

    测试环境:VMware Workstation / centos7 1.磁盘分区 (1)易于管理和使用: 比如说我们把磁盘分了sda1.sda2.sda3.sda4盘,我们假设sda1盘为系统盘,其他 ...

  4. Codeforces Round #421 (Div. 1) (BC)

    1. 819B Mister B and PR Shifts 大意: 给定排列$p$, 定义排列$p$的特征值为$\sum |p_i-i|$, 可以循环右移任意位, 求最小特征值和对应移动次数. 右移 ...

  5. Docker学习笔记导航帖

    1. Docker安装 安装docker https://www.cnblogs.com/kreo/p/10813010.html

  6. Java8新特性 - Java内置的四大核心函数式接口

    Java内置的四大核心函数式接口 Consumer:消费型接口 对类型为T的对象应用操作,包含方法:void accept(T t) public class TestLambda02 { publi ...

  7. Django_rest_framework分页

    分页基本流程及配置 1.基于LimitOffsetPagination做分页,根据配置 from rest_framework.pagination import LimitOffsetPaginat ...

  8. 在Eclipse配置Tomcat服务器+JSP实例创建

    欢迎任何形式的转载,但请务必注明出处. 1.jdk安装及环境配置 点击进入教程 2.Eclipse安装 点击进入官网下载 注意下载完成打开.exe后,出现的界面,有很多版本供选择.选择下图版本 3.T ...

  9. How to : SAP PI Cache Refresh

    Requirement : Identify various tools/resources available to perform SAP PI Cache refresh . Please no ...

  10. Python中with用法详解

    一 .with语句的原理 上下文管理协议(Context Management Protocol):包含方法 __enter__()和__exit__(),支持该协议的对象要实现这两个方法. 上下文管 ...