日期:2014-05-16  浏览次数:20344 次

关于JavaScript中计算精度丢失的问题(一)
摘要:
由于计算机是用二进制来存储和处理数字,不能精确表示浮点数,而JavaScript中没有相应的封装类来处理浮点数运算,直接计算会导致运算精度丢失。
为了避免产生精度差异,把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完毕再降级(除以10的n次幂),这是大部分编程语言处理精度差异的通用方法。
关键词:
计算精度 四舍五入 四则运算 精度丢失
1. 疑惑
我们知道,几乎每种编程语言都提供了适合货币计算的类。例如C#提供了decimal,Java提供了BigDecimal,JavaScript提供了Number……
由于之前用decimal和BigDecimal用得很好,没有产生过精度问题,所以一直没有怀疑过JavaScript的Number类型,以为可以直接使用Number类型进行计算。但是直接使用是有问题的。
我们先看看四舍五入的如下代码:
alert(Number(0.009).toFixed(2));
alert(Number(162.295).toFixed(2));

按正常结果,应该分别弹出0.01和162.30。但实际测试结果却是在不同浏览器中得到的是不同的结果:
在ie6、7、8下得到0.00和162.30,第一个数截取不正确;
在firefox中得到0.01和162.29,第二个数截取不正确;
在opera下得到0.01和162.29,第二个数截取不正确
我们再来看看四则运算的代码:
alert(1/3);//弹出: 0.3333333333333333
alert(0.1 + 0.2);//弹出: 0.30000000000000004 
alert(-0.09 - 0.01);//弹出: -0.09999999999999999
alert(0.012345 * 0.000001);//弹出: 1.2344999999999999e-8
alert(0.000001 / 0.0001);//弹出: 0.009999999999999998

按正常结果,除第一行外(因为其本身就不能除尽),其他都应该要得到精确的结果,从弹出的结果我们却发现不是我们想要的正确结果。是因为没有转换成Number类型吗?我们转换成Number后再计算看看:
alert(Number(1)/Number(3));//弹出: 0.3333333333333333   	
alert(Number(0.1) + Number(0.2));//弹出: 0.30000000000000004    
alert(Number(-0.09) – Number(0.01));//弹出: -0.09999999999999999   
alert(Number(0.012345) * Number(0.000001));//弹出: 1.2344999999999999e-8   
alert(Number(0.000001) / Number(0.0001));//弹出: 0.009999999999999998

还是一样的结果,看来javascript默认把数字识别为number类型。为了验证这一点,我们用typeof弹出类型看看:
alert(typeof(1));//弹出: number
alert(typeof(1/3));//弹出: number
alert(typeof(-0.09999999));//弹出: number

2. 原因
为什么会产生这种精度丢失的问题呢?是javascript语言的bug吗?
我们回忆一下大学时学过的计算机原理,计算机执行的是二进制算术,当十进制数不能准确转换为二进制数时,这种精度误差就在所难免。
再查查javascript的相关资料,我们知道javascript中的数字都是用浮点数表示的,并规定使用IEEE 754 标准的双精度浮点数表示:
IEEE 754 规定了两种基本浮点格式:单精度和双精度。
  IEEE单精度格式具有24 位有效数字精度(包含符号号),并总共占用32 位。
  IEEE双精度格式具有53 位有效数字精度(包含符号号),并总共占用64 位。
这种结构是一种科学表示法,用符号(正或负)、指数和尾数来表示,底数被确定为2,也就是说是把一个浮点数表示为尾数乘以2的指数次方再加上符号。下面来看一下具体的规格:
        符号位         指数位         小数部分 指数偏移量
单精度浮点数 1位(31) 8位(30-23) 23位(22-00) 127
双精度浮点数 1位(63) 11位(62-52) 52位(51-00) 1023

我们以单精度浮点数来说明:
指数是8位,可表达的范围是0到255
而对应的实际的指数是-127到+128
这里特殊说明,-127和+128这两个数据在IEEE当中是保留的用作多种用途的
-127表示的数字是0
128和其他位数组合表示多种意义,最典型的就是NAN状态。
知道了这些,我们来模拟计算机的进制转换的计算,就找一个简单的0.1+0.2来推演吧(引用自http://blog.csdn.net/xujiaxuliang/archive/2010/10/13/5939573.aspx):
十进制0.1  
 => 二进制0.00011001100110011…(循环0011)   
 =>尾数为1.1001100110011001100…1100(共52位,除了小数点左边的1),指数为-4(二进制移码为00000000010),符号位为0  
 => 计算机存储为:0 00000000100 10011001100110011…11001  
 => 因为尾数最多52位,所以实际存储的值为0.00011001100110011001100110011001100110011001100110011001  
 而十进制0.2  
 => 二进制0.0011001100110011…(循环0011)  
 =>尾数为1.1001100110011001100…1100(共52位,除了小数点左边的1),指数为-3(二进制移码为00000000011),符号位为0  
 => 存储为:0 00000000011 10011001100110011…11001  
 因为尾数最多52位,所以实际存储的值为0.00110011001100110011001100110011001100110011001100110011  
 那么两者相加得:      
 0.00011001100110011001100110011001100110011001100110011001  
+  0.00110011001100110011001100110011001100110011001100110011
 =  0.01001100110011001100110011001100110011001100110011001100  
 转换成10进制之后得到:0.30000000000000004

从上述的推演过程我们知道,这种误差是难免的,c#的decimal和Java的BigDecimal之所以没有出现精度差异,只是因为在其内部作了相应处理,把这种精度差异给屏蔽掉了,而javascript是一种弱类型的脚本语言,本身并没有对计算精度做相应的处理,这就需要我们另外想办法处理了。
3. 解决办法
3.1 升级降级
从上文我们已经知道,javascript中产生精度差异的原因是计算机无法精确表示浮点数,连自身都不能精确,运算起来就更加得不到精确的结果了。那么怎么让计算机精确认识要计算的数呢?
我们知道十进制的整数和二进制是可以互相进行精确转换的,那么我们把浮点数升级(乘以10的n次幂)成计算机能够精确识别的整数