不知道大家在使用JS的过程中有没有发现某些浮点数运算的时候,得到的结果存在精度问题:比如0.1 + 0.2 = 0.30000000000000004以及7 * 0.8 = 5.6000000000000005等等。

究竟是什么原因造成了这个问题?实际上是因为计算机内部的信息都是由二进制方式表示的,即0和1组成的各种编码,但由于某些浮点数没办法用二进制准确的表示出来,也就带来了一系列精度问题。当然这也不是JS独有的问题

接下来让我们以 0.1+0.2 为例,深入理解一下浮点数的运算方法,以及使用JS时应该如何规避这个问题。这个问题很基础,但也很有了解的必要,大家就当是复习一下《计算机组成原理》吧。

通过后面的几个小章节,将会大致为大家介绍以下几个方面内容:

● 浮点数的二进制表示方式
● IEEE 754 标准是什么
● 避开浮点数计算精度问题的方案
● 测试框架(Mocha)的基本用法

1. 计算机的运算方式

(1)如何将小数转化成二进制

① 整数部分:除2取余数,若商不为0则继续对它除2,当商为0时则将所有余数逆序排列;

② 小数部分:乘2取整数部分,若小数不为0则继续乘2,直至小数部分为0将取出的整数位正序排列。(若小数部分无法为零,根据有效位数要求取得相应数值,位数后一位0舍1入进行取舍)

利用上述方法,我们尝试一下将0.1转成二进制:

0.1 * 2 = 0.2 - - - - - - - - - - 取0

0.2 * 2 = 0.4 - - - - - - - - - - 取0

0.4 * 2 = 0.8 - - - - - - - - - - 取0

0.8 * 2 = 1.6 - - - - - - - - - - 取1

0.6 * 2 = 1.2 - - - - - - - - - - 取1

0.2 * 2 = 0.4 - - - - - - - - - - 取0

......

算到这就会发现小数部分再怎么继续乘都不会等于0,所以二进制是没办法精确表示0.1的。 那么0.1的二进制表示是:0.000110011......0011...... (0011无限循环) 而0.2的二进制表示则是:0.00110011......0011...... (0011无限循环) 而具体应该保存多少位数,则需要根据使用的是什么标准来确定,也就是下一节所要讲到的内容。

(2)IEEE 754 标准

IEEE 754 标准是IEEE二进位浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号。IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数自述的交换、算术格式以及方法。 根据IEEE 754标准,任意一个二进制浮点数都可以表示成以下形式:

S为数符,它表示浮点数的正负(0正1负);M为有效位(尾数);E为阶码,用移码表示,阶码的真值都被加上一个常数(偏移量)。 尾数部分M通常都是规格化表示的,即非"0"的尾数其第一位总是"1",而这一位也称隐藏位,因为存储时候这一位是会被省略的。比如保存1.0011时,只保存0011,等读取的时候才把第一位的1加上去,这样做相当于多保存了1位有效数字。

常用的浮点格式有:
① 单精度:

这是32位的浮点数,最高的1位是符号位S,后面的8位是指数E,剩下的23位为尾数(有效数字)M;

真值为:

② 双精度:

这是64位的浮点数,最高的1位是符号位S,后面的11位是指数E,剩下的52位为尾数(有效数字)M;

真值为:

JavaScript只有一种数字类型number,而number使用的就是IEEE 754双精度浮点格式。

依据上述规则,接下来我们就来看看 JS 是如何存储 0.1 和 0.2 的:

0.1 是正数,所以符号位是0;

而其二进制位是 0.000110011......0011...... ( 0011 无限循环),进行规格化后为1.10011001......1001(1)*2^-4,根据0舍1入的规则,最后的值为

2^-4 * 1.1001100110011001100110011001100110011001100110011010

而指数 E = -4 + 1023 = 1019 由此可得,JS中 0.1 的二进制存储格式为(符号位用逗号分隔,指数位用分号分隔): 0,01111111011;1001100110011001100110011001100110011001100110011010

0.2 则为0,01111111100;1001100110011001100110011001100110011001100110011010

Q1:指数位E(阶码)为何用移码表示?
A1:为了便于判断其大小。

(3)浮点运算

0.1 => 0,01111111011;1001100110011001100110011001100110011001100110011010 0.2 => 0,01111111100;1001100110011001100110011001100110011001100110011010 浮点数的加减运算按以下几步进行: ① 对阶,使两数的小数点位置对齐(也就是使两数的阶码相等)。 所以要先求阶差,阶小的尾数要根据阶差来右移(尾数位移时可能会发生数丢失的情况,影响精度) 因为0.1和0.2的阶码和尾数均为正数,所以它们的原码、反码及补码都是一样的。(使用补码进行运算,计算过程中使用双符号) △阶差(补码) = 00,01111111011 - 00,01111111100 = 00,01111111011 + 11,10000000100 = 11,11111111111 由上可知△阶差为-1,也就是0.1的阶码比0.2的小,所以要把0.1的尾数右移1位,阶码加1(使0.1的阶码和0.2的一致) 最后0.1 => 0,01111111100;1100110011001100110011001100110011001100110011001101(0) 注:要注意0舍1入的原则。之所以右移一位,尾数补的是1,是因为隐藏位的数值为1(默认是不存储的,只有读取的时候才加上

② 尾数求和

0.1100110011001100110011001100110011001100110011001101 + 1.1001100110011001100110011001100110011001100110011010 —————————————————————————————— 10.0110011001100110011001100110011001100110011001100111

③ 规格化 针对步骤②的结果,需要右规(即尾数右移1位,阶码加1) sum = 0.1 + 0.2 = 0,01111111101;1.0011001100110011001100110011001100110011001100110011(1) 注:右规操作,可能会导致低位丢失,引起误差,造成精度问题。所以就需要步骤④的舍入操作

④ 舍入(0舍1入)

sum = 0,01111111101;1.0011001100110011001100110011001100110011001100110100

⑤ 溢出判断

根据阶码判断浮点运算是否溢出。而我们的阶码 01111111101 即不上溢,也不下溢。

至此,0.1+0.2的运算就已经结束了。接下来,我们一起来看看上面计算得到的结果,它的十进制数是多少。

<1> 先将它非规格化,得到二进制形式:

sum = 0.010011001100110011001100110011001100110011001100110100

<2> 再将其转成十进制

sum = 2^2 + 2^5 + 2^6 + ... + 2^52 = 0.30000000000000004440892098500626

现在你应该明白JS中 0.30000000000000004 这个结果怎么来的吧。

Q2:计算机运算为何要使用补码?

A2:可以简化计算机的运算步骤,且只用设加法器,如做减法时若能找到与负数等价的正数来代替该负数,就可以把减法操作用加法代替。而采用补码,就能达到这个效果。

2. 浮点精度问题的解决办法

(1)简单解决方案

我的思路就是将小数转成整数来运算,之后再转回小数。代码也比较简单,就直接贴出来了。

'use strict'var accAdd = function(num1, num2) {num1 = Number(num1);num2 = Number(num2);var dec1, dec2, times;try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }times = Math.pow(10, Math.max(dec1, dec2));// var result = (num1 * times + num2 * times) / times;var result = (accMul(num1, times) + accMul(num2, times)) / times;return getCorrectResult("add", num1, num2, result);// return result;
};var accSub = function(num1, num2) {num1 = Number(num1);num2 = Number(num2);var dec1, dec2, times;try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }times = Math.pow(10, Math.max(dec1, dec2));// var result = Number(((num1 * times - num2 * times) / times);var result = Number((accMul(num1, times) - accMul(num2, times)) / times);return getCorrectResult("sub", num1, num2, result);// return result;
};var accDiv = function(num1, num2) {num1 = Number(num1);num2 = Number(num2);var t1 = 0, t2 = 0, dec1, dec2;try { t1 = countDecimals(num1); } catch (e) { }try { t2 = countDecimals(num2); } catch (e) { }dec1 = convertToInt(num1);dec2 = convertToInt(num2);var result = accMul((dec1 / dec2), Math.pow(10, t2 - t1));return getCorrectResult("div", num1, num2, result);// return result;
};var accMul = function(num1, num2) {num1 = Number(num1);num2 = Number(num2);var times = 0, s1 = num1.toString(), s2 = num2.toString();try { times += countDecimals(s1); } catch (e) { }try { times += countDecimals(s2); } catch (e) { }var result = convertToInt(s1) * convertToInt(s2) / Math.pow(10, times);return getCorrectResult("mul", num1, num2, result);// return result;
};var countDecimals = function(num) {var len = 0;try {num = Number(num);var str = num.toString().toUpperCase();if (str.split('E').length === 2) { // scientific notationvar isDecimal = false;if (str.split('.').length === 2) {str = str.split('.')[1];if (parseInt(str.split('E')[0]) !== 0) {isDecimal = true;}}let x = str.split('E');if (isDecimal) {len = x[0].length;}len -= parseInt(x[1]);} else if (str.split('.').length === 2) { // decimalif (parseInt(str.split('.')[1]) !== 0) {len = str.split('.')[1].length;}}} catch(e) {throw e;} finally {if (isNaN(len) || len < 0) {len = 0;}return len;}
};var convertToInt = function(num) {num = Number(num);var newNum = num;var times = countDecimals(num);var temp_num = num.toString().toUpperCase();if (temp_num.split('E').length === 2) {newNum = Math.round(num * Math.pow(10, times));} else {newNum = Number(temp_num.replace(".", ""));}return newNum;
};var getCorrectResult = function(type, num1, num2, result) {var temp_result = 0;switch (type) {case "add":temp_result = num1 + num2;break;case "sub":temp_result = num1 - num2;break;case "div":temp_result = num1 / num2;break;case "mul":temp_result = num1 * num2;break;}if (Math.abs(result - temp_result) > 1) {return temp_result;}return result;
};复制代码

基本用法: 

加法: accAdd(0.1, 0.2) // 得到结果:0.3

减法: accSub(1, 0.9) // 得到结果:0.1

除法: accDiv(2.2, 100) // 得到结果:0.022

乘法: accMul(7, 0.8) // 得到结果:5.6

countDecimals()方法:计算小数位的长度

convertToInt()方法:将小数转成整数

getCorrectResult()方法:确认我们的计算结果无误,以防万一

3. 总结:

JS浮点数计算精度问题是因为某些小数没法用二进制精确表示出来。JS使用的是IEEE 754双精度浮点规则。 而规避浮点数计算精度问题,可通过以下几种方法:

● 调用round() 方法四舍五入或者toFixed() 方法保留指定的位数(对精度要求不高,可用这种方法)

● 将小数转为整数再做计算,即前文提到的那个简单的解决方案

● 使用特殊的进制数据类型,如前文提到的bignumber(对精度要求很高,可借助这些相关的类库)

谈 JavaScript 浮点数计算精度问题(如0.1+0.2!==0.3)相关推荐

  1. 如何避开JavaScript浮点数计算精度问题(如0.1+0.2!==0.3)

    不知道大家在使用JS的过程中有没有发现某些浮点数运算的时候,得到的结果存在 精度问题:比如0.1 + 0.2 = 0.30000000000000004以及7 * 0.8 = 5.6000000000 ...

  2. JavaScript 格式化数字、金额、千分位、保留几位小数、舍入舍去… 及其浮点数计算精度问题(推荐的类库 Numeral.js 和 accounting.js)...

    前端开发中经常会碰到用 JavaScript 格式化数字,最最常见的是格式化金额,一般格式化金额需要千分位分隔,保留2位小数等等. 还有偶然会遇到的使用 js 计算浮点数时出现误差. 完善的功能函数推 ...

  3. JavaScript学习(六十四)—关于JS的浮点数计算精度问题解决方案

    JavaScript学习(六十四)-关于JS的浮点数计算精度问题解决方案 您的语言没有中断,它正在执行浮点数学运算.计算机只能本地存储整数,因此它们需要某种表示十进制数字的方式.此表示并不完全准确.这 ...

  4. 0.1+0.2不等于0.3,微信小程序云开发如何解决JavaScript小数计算精度失准的问题

    先看图,0.1+0.2不等于0.3 这个是JavaScript语言自身存在的一个问题.说到这里不得不提一下网上流传的JavaScript搞笑图 我们在使用云开发来开发微信小程序的时候,会经常遇到Jav ...

  5. PHP 浮点数计算精度问题

    近日计算价格时,0.91 + 0.1 = 0.91999999: 查看了各种论坛后,发现是浮点数计算精度问题造成的 浮点数运算精度丢失的产生原因 ​ 在计算机中,只有二进制的数据才能被识别和处理.所以 ...

  6. 关于float浮点数计算精度问题的深入分析

    在Java中,或者说在任何一门语言中,float和double两种类型浮点数计算的精度问题都是老生常谈了.在进行跟钱相关的计算时(毕竟是白花花的银子)的时候如果不注意这两种浮点类型往往会出现许多莫名其 ...

  7. 用decimal.js库解决JavaScript中计算精度丢失的问题

    项目场景: 涉及小数点的计算 精度丢失原因看这里 解决 用decimal.js库,decimal.js是使用的二进制来计算的, 所以能解决js的精度问题. 安装和引入 执行命令npm i decima ...

  8. js 单精度浮点数转10进制_确保前端 JavaScript 浮点数精度的四则运算方法

    1 浮点数运算与 IEEE 754 标准 在 JavaScript 中,执行 0.1+0.2,得到的结果却是 0.30000000000000004.这就不得不提到 IEEE 754 标准. IEEE ...

  9. 关于浮点数计算时的精度问题

    那个有问题的缩略图生成的方法发布之后,短短半天就有很多朋友响应,其中指出了不少方法中的不少问题,有些也是我没有意识到的.果然集体的智慧是无穷的,一段代码在许多人的眼皮底下经过,想留有bug也不容易.不 ...

最新文章

  1. webpack 项目使用webpack-dev-server 自动编译 (2)
  2. CYQ.Data 轻量数据层之路 使用篇三曲 MAction 取值赋值(十四)
  3. rpg制作大师2003_RPG制作大师MV 我们一起做游戏(十四)
  4. Delphi中的异常处理
  5. 论文笔记_S2D.73_2019_BTS_从大到小:多尺度局部平面引导的单目深度估计
  6. 从零基础入门Tensorflow2.0 ----七、33 数据padding,模型构建,训练
  7. Django 文件下载
  8. 酷狗歌词Krc批量转换工具Lrc [附转换编码DLL]
  9. Unity3d 发动机原理详细介绍
  10. 【知识兔】两列Excel数据快速合并为一列,你会哪种方法?
  11. 2008r2 请检查名称的拼写_甲状腺素、甲状腺激素、T3、T4…这些名称你分得清吗?...
  12. iphone原彩显示对眼睛好吗_iPhone12又拉胯?用户吐槽屏幕发黄,到底是为啥?
  13. 在vue页面监听中如何修改子元素的样式
  14. 多边形网格到B-Rep实体转换:算法详细信息和C ++代码示例
  15. python编程图文_深入Python多进程编程基础——图文版
  16. SWAP函数的几种写法
  17. halcon-检测圆弧拟合圆
  18. RTSP安防网络摄像头/海康大华硬盘录像机网页无插件直播方案EasyNVR之主要功能模块及相关技术特点与性能指标分析
  19. css实现文字纵向排版并且水平垂直居中
  20. PHP Framework YII的里的gii设置。

热门文章

  1. MicroSoft的Office使用攻略
  2. __MACOSX文件是什么
  3. 行业洞见 | 一文了解自动驾驶汽车
  4. 发新款电池!成本降低14%,特斯拉会继续降价吗?
  5. RISC-V浪潮来袭!115页PPT超详论述,如何与ARM争锋!(一)
  6. 波士顿动力机器狗量产版首次亮相:先造100台,能当警犬能工地巡逻
  7. 量子计算赛道上的巨头拉锯战
  8. 中国机器人产业发展报告(2018)正式发布!
  9. 密歇根大学联合谷歌大脑提出,通过「推断语义布局」实现「文本到图像合成」
  10. 壕!阿里开工红包惊人,最高 1000 万,有人却只收到一杯白开水