javascript 数字精度问题

来源:http://rockyee.iteye.com/blog/891538

摘要:
由于计算机是用二进制来存储和处理数字,不能精确表示浮点数,而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次幂)成计算机能够精确识别的整数来计算,计算完毕之后再降级(除以10的n次幂),不就得到精确的结果了吗?好,就这么办!
我们知道,Math.pow(10,scale)可以得到10的scale次方,那么就把浮点数直接乘以Math.pow(10,scale)就可以了吗?我最初就是这么想的,但后来却发现一些数字运算后实际结果与我们的猜想并不一致。我们来看看这个简单的运算:

alert(512.06*100); 

按常理应该返回51206,但实际结果却是51205.99999999999。奇怪吧?其实也不奇怪,这是因为浮点数不能精确参与乘法运算,即使这个运算很特殊(只是乘以10的scale次方进行升级)。如此我们就不能直接乘以10的scale次方进行升级,那就让我们自己来挪动小数点吧。
怎么挪动小数点肯定大家是各有妙招,此处附上我写的几个方法:

/**  
* 左补齐字符串  
*   
* @param nSize  
*            要补齐的长度  
* @param ch  
*            要补齐的字符  
* @return  
*/  
String.prototype.padLeft = function(nSize, ch)   
{   
var len = 0;   
var s = this ? this : "";   
ch = ch ? ch : '0';// 默认补0   
len = s.length;   
while (len < nSize)   
{   
s = ch + s;   
len++;   
}   
return s;   
}   
/**  
* 右补齐字符串  
*   
* @param nSize  
*            要补齐的长度  
* @param ch  
*            要补齐的字符  
* @return  
*/  
String.prototype.padRight = function(nSize, ch)   
{   
var len = 0;   
var s = this ? this : "";   
ch = ch ? ch : '0';// 默认补0   
len = s.length;   
while (len < nSize)   
{   
s = s + ch;   
len++;   
}   
return s;   
}   
/**  
* 左移小数点位置(用于数学计算,相当于除以Math.pow(10,scale))  
*   
* @param scale  
*            要移位的刻度  
* @return  
*/  
String.prototype.movePointLeft = function(scale)   
{   
var s, s1, s2, ch, ps, sign;   
ch = '.';   
sign = '';   
s = this ? this : "";   
if (scale <= 0) return s;   
ps = s.split('.');   
s1 = ps[0] ? ps[0] : "";   
s2 = ps[1] ? ps[1] : "";   
if (s1.slice(0, 1) == '-')   
{   
s1 = s1.slice(1);   
sign = '-';   
}   
if (s1.length <= scale)   
{   
ch = "0.";   
s1 = s1.padLeft(scale);   
}   
return sign + s1.slice(0, -scale) + ch + s1.slice(-scale) + s2;   
}   
/**  
* 右移小数点位置(用于数学计算,相当于乘以Math.pow(10,scale))  
*   
* @param scale  
*            要移位的刻度  
* @return  
*/  
String.prototype.movePointRight = function(scale)   
{   
var s, s1, s2, ch, ps;   
ch = '.';   
s = this ? this : "";   
if (scale <= 0) return s;   
ps = s.split('.');   
s1 = ps[0] ? ps[0] : "";   
s2 = ps[1] ? ps[1] : "";   
if (s2.length <= scale)   
{   
ch = '';   
s2 = s2.padRight(scale);   
}   
return s1 + s2.slice(0, scale) + ch + s2.slice(scale, s2.length);   
}   
/**  
* 移动小数点位置(用于数学计算,相当于(乘以/除以)Math.pow(10,scale))  
*   
* @param scale  
*            要移位的刻度(正数表示向右移;负数表示向左移动;0返回原值)  
* @return  
*/  
String.prototype.movePoint = function(scale)   
{   
if (scale >= 0)   
return this.movePointRight(scale);   
else  
return this.movePointLeft(-scale);   
}  


这样我们升级降级都可以转换成字符串后调用String对象的自定义方法movePoint了,乘以10的scale次方我们传正整数scale,除以10的scale次方我们传负整数-scale。
再来看看我们之前升级512.06的代码,采用自定义方法的调用代码变成这样:

alert(512.06.toString().movePoint(2)); //弹出: 51206  

这样直接挪动小数点就不怕它不听话出现一长串数字了(*^__^*)。 当然,movePoint方法得到的结果是字符串,如果要转成Number类型也很方便(怎么转就不再废话了)。
 

 3.2 四舍五入

 好,有了升级降级的基础,我们来看看四舍五入的方法,由于不同浏览器对Number的toFixed方法有不同的支持,我们需要用自己的方法去覆盖浏览器的默认实现。
有一个简单的办法是我们自己来判断要截取数据的后一位是否大于等于5,然后进行舍或者入。我们知道Math.ceil方法是取大于等于指定数的最小整数,Math.floor方法是取小于等于指定数的最大整数,于是我们可以利用这两个方法来进行舍入处理,先将要进行舍入的数升级要舍入的位数scale(乘以10的scale次方),进行ceil或floor取整后,再降级要舍入的位数scale(除以10的scale次方)。
代码如下

Number.prototype.toFixed = function(scale)   
{   
var s, s1, s2, start;   
s1 = this + "";   
start = s1.indexOf(".");   
s = s1.movePoint(scale);   
if (start >= 0)   
{   
s2 = Number(s1.substr(start + scale + 1, 1));   
if (s2 >= 5 && this >= 0 || s2 < 5 && this < 0)   
{   
s = Math.ceil(s);   
}   
else  
{   
s = Math.floor(s);   
}   
}   
return s.toString().movePoint(-scale);   
}

覆盖Number类型的toFixed方法后,我们再来执行以下方法

alert(Number(0.009).toFixed(2));//弹出0.01   
alert(Number(162.295).toFixed(2));//弹出162.30  

在ie6、7、8、firefox、Opera下分别进行验证,都能得到相应的正确的结果。
另一种方式是在网上找到的采用正则表达式来进行四舍五入,代码如下:

Number.prototype.toFixed = function(scale)   
{   
var s = this + "";   
if (!scale) scale = 0;   
if (s.indexOf(".") == -1) s += ".";   
s += new Array(scale + 1).join("0");   
if (new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (scale + 1) + "})?)\\d*$").test(s))   
{   
var s = "0" + RegExp.$2, pm = RegExp.$1, a = RegExp.$3.length, b = true;   
if (a == scale + 2)   
{   
a = s.match(/\d/g);   
if (parseInt(a[a.length - 1]) > 4)   
{   
for (var i = a.length - 2; i >= 0; i--)   
{   
a[i] = parseInt(a[i]) + 1;   
if (a[i] == 10)   
{   
a[i] = 0;   
b = i != 1;   
}   
else  
break;   
}   
}   
s = a.join("").replace(new RegExp("(\\d+)(\\d{" + scale + "})\\d$"), "$1.$2");   
}   
if (b) s = s.substr(1);   
return (pm + s).replace(/\.$/, "");   
}   
return this + "";   
}  

经验证,这两个方法都能够进行准确的四舍五入,那么采用哪个方法好呢?实践出真知,我们写一个简单的方法来验证一下两种方式的性能:

 

function testRound()   
{   
var dt, dtBegin, dtEnd, i;   
dtBegin = new Date();   
for (i=0; i<100000; i++)   
{   
dt = new Date();   
Number("0." + dt.getMilliseconds()).toFixed(2);   
}   
dtEnd = new Date();   
alert(dtEnd.getTime()-dtBegin.getTime());   
} 

为了避免对同一个数字进行四舍五入运算有缓存问题,我们取当前毫秒数进行四舍五入。经验证,在同一台机器上运算10万次的情况下,用movePoint方法,平均耗时2500毫秒;用正则表达式方法,平均耗时4000毫秒。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/497713.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在js中使用ajax请求数据,在 JS 中怎么使用 Ajax 来进行请求

在 JS 中怎么使用 Ajax 来进行请求发布时间&#xff1a;2021-07-22 09:48:43来源&#xff1a;亿速云阅读&#xff1a;78作者&#xff1a;chen本篇内容介绍了“在 JS 中怎么使用 Ajax 来进行请求”的有关知识&#xff0c;在实际案例的操作过程中&#xff0c;不少人都会遇到这样的…

5G年终盘点 | 2017年里的9个“万万没想到”

来源&#xff1a;华为概要&#xff1a;回首2017&#xff0c;5G的发展可谓风起云涌&#xff0c;我们一次次地被刷新着认知&#xff0c;感叹5G的奇妙。时光荏苒&#xff0c;白驹过隙&#xff0c;转眼就到了年底。回首2017&#xff0c;5G的发展可谓风起云涌&#xff0c;我们一次次…

java 数字计算精度问题

问题的提出&#xff1a; 编译运行下面这个程序会看到什么&#xff1f; public class Test{ public static void main(String args[]){ System.out.println(0.050.01); System.out.println(1.0-0.42); System.out.println(4.015*100); …

idc服务器管理系统勇士水花,idc管理系统

该系统可实现供配电、UPS、空调、温湿度、消防、安防、漏水检测、视频等环境保障设备的机房系统。机房内各设备的可靠与否直接关系着网络、服务器等设备能否正常、持久、稳定的运行&#xff0c;因此机房环境动力监控系统得到了日益广泛的应用&#xff0c;不仅提高设备的维护管理…

透视 CES 2018:不容错过的四大科技趋势

来源&#xff1a;新智造概要&#xff1a;物理与数字世界正走向融合&#xff0c;我们每天醒来的时间、睡眠时长、心率和步数等数据都会被分享、上传并转化为分析数据。物理与数字世界正走向融合&#xff0c;我们每天醒来的时间、睡眠时长、心率和步数等数据都会被分享、上传并转…

单片机wifi模块与服务器通信协议,单片机常用的几种通信协议

在单片机的应用中&#xff0c;通信协议是其中必不可少的一部分&#xff0c;上位机与下位机&#xff0c;单片机与单片机&#xff0c;单片机与外设模块之间的通信都需要通信协议实现信息交换和资源共享。由于设备之间不同的传输速率、电气特性、可靠性要求的不同&#xff0c;也产…

Axis2;wsdl生成客户端和serverJava代码

来源&#xff1a;http://blog.csdn.net/ouyangtianhan/article/details/6779528 1.安装JDK6 并设置环境变量 JAVA_HOME,path,class 本文的安装路径为: D:\Tools\JDK6 故设置&#xff1a;JAVA_HOMED:\Tools\JDK6 path%JAVA_HOME%\bin; classpath.;%JA…

Gartner预测:2025年,人工智能将创造200万个新增就业机会

来源&#xff1a;人工智能和大数据概要&#xff1a;对于与人工智能&#xff08;AI&#xff09;有关的就业动态来讲&#xff0c;2020年将是极其重要的一年&#xff0c;人工智能将带动整体工作机会的正增长。对于与人工智能&#xff08;AI&#xff09;有关的就业动态来讲&#xf…

安装服务器系统多少钱,服务器系统安装费用

服务器系统安装费用 内容精选换一换安装完操作系统后的临时云服务器还需要进行相关配置&#xff0c;并安装云平台提供的Guest OS driver&#xff0c;才能保证后续创建的云服务器正常使用。Guest OS driver包括VMTools驱动和PV driver&#xff0c;在前面步骤中已为云服务器安装V…

未来15年,人工智能将带给城市8种改变

来源&#xff1a;微软研究院概要&#xff1a;如果到了2030年&#xff0c;人工智能将给北美城市的面貌带来怎样的平均标准变化呢&#xff1f;如果到了2030年&#xff0c;人工智能将给北美城市的面貌带来怎样的平均标准变化呢&#xff1f;专家们对人工智能进行了一个世纪的研究&a…

axis2 webservice入门学识(JS,Java,PHP调用实例源码)

来源&#xff1a;http://www.myexception.cn/web/952419.html axis2 webservice入门知识(JS,Java,PHP调用实例源码)背景简介最近接触到一个银行接口的案子&#xff0c;临时需要用到axis2 webservice。自己现学现总结的一些东西&#xff0c;留给新手。少走弯路。Axis2简介①采用…

网络系统服务器子系统,网管系统中服务器及网络设备监控子系统的设计与实现...

摘要&#xff1a;近年来,计算机网络的发展特点是规模不断扩大,复杂性不断增加,异构性越来越高,从而增加了网络管理的难度。面对越来越复杂和重要的网络,如何确保其尽可能长时间的正常运行,或当网络出现故障时,尽可能快地发现和修复故障,使其最大限度地发挥其应用功能和效益,就成…

普华永道:人工智能将重塑职位格局并与物联网合并

来源&#xff1a;亿欧概要&#xff1a;人工智能正在迅速普及&#xff0c;且其普及程度决定了其能为企业带来何种规模的效益。人工智能正在迅速普及&#xff0c;且其普及程度决定了其能为企业带来何种规模的效益。人工智能的核心在于以各种机器模拟智能行为&#xff0c;而物联网…

log4j 控制台和文件输出乱码问题解决

来源&#xff1a;http://www.coderli.com/log4j-console-file-garbled 一个小问题&#xff0c;却让我感觉到&#xff0c;现在真正动脑的人很少。。我来说说吧。今天遇到一个小问题&#xff0c;log4j输出到文件乱码&#xff0c;控制台正常。显然是编码问题导致。Google一搜&…

电脑没网络设备dns服务器没检测到响应,设备或资源dns没检测到有响应 网络无法连接...

以电脑为例&#xff0c;提示设备或资源dns没检测到有响应网络无法连接的原因是&#xff1a;1、可能是DNS解析不了&#xff0c;这时候可以看看自己的电脑的DNS是手动获取还是自动获取&#xff0c;如果是手动获取的话&#xff0c;改为自动获取。2、可能和网络设备或者网络环境有关…

asp.net ajax 怎么获取前端ul li_useEffect Hook 是如何工作的(前端需要懂的知识点)

作者&#xff1a;Dave Ceddia译者&#xff1a;前端小智来源&#xff1a;daveceddia.为了保证的可读性&#xff0c;本文采用意译而非直译。想象一下:你有一个非常好用的函数组件&#xff0c;然后有一天&#xff0c;咱们需要向它添加一个生命周期方法。呃…刚开始咱们可能会想怎么…

2018年全球5G的12大趋势

来源&#xff1a;5G概要&#xff1a;2018年全球5G的12大趋势行业观察未来智能实验室是人工智能学家与科学院相关机构联合成立的人工智能&#xff0c;互联网和脑科学交叉研究机构。由互联网进化论作者&#xff0c;计算机博士刘锋与中国科学院虚拟经济与数据科学研究中心石勇、刘…

java运行命令解释

-Dfile.encoding解释&#xff1a; 在命令行中输入java&#xff0c;在给出的提示中会出现-D的说明&#xff1a; -D<name><value> set a system property -D后面需要跟一个键值对&#xff0c;作用是通过命令行向java虚拟机传递一项系统属性 对-Dfile.…

服务器销售考核方案,电商后台:运营绩效系统总结

文章内容做者分离出来自身历经取每个人共享了电子商务情况中的运营业绩考核体系。业绩考核体系是明年5月份诸位发布的&#xff0c;通过一段时间运用&#xff0c;创造发明体系存已经一系列不了控果素&#xff0c;因此可以衷于一期的业绩考核体系虽然每一个月可以出示相关统计分析…

create 添加async和不添加的区别_鸽子饮水添加剂肝精与电解质的区别,不能混淆也不能代替...

肝精与电解质是鸽友们常用的两种饮水添加剂&#xff0c;虽说肝精与电解质都有清除药物残留的作用&#xff0c;但是&#xff0c;这两种添加剂的性质和功效是不一样的。有的混淆不清用电解质代替肝精&#xff0c;那是不对的。肝精就是肝精&#xff0c;电解质就是电解质&#xff0…