JS浮点数运算结果不精确的Bug解决


Posted in Javascript onAugust 01, 2019

前言

最近在做项目的时候,涉及到产品价格的计算,经常会出现JS浮点数精度问题,这个问题,对于财务管理系统的开发者来说,是个非常严重的问题(涉及到钱相关的问题都是严重的问题),这里把相关的原因和问题的解决方案整理一下,也希望给各位提供一些参考。

一. 常见例子  

// 加法
 0.1 + 0.2 = 0.30000000000000004
 0.1 + 0.7 = 0.7999999999999999
 0.2 + 0.4 = 0.6000000000000001

 // 减法
 0.3 - 0.2 = 0.09999999999999998
 1.5 - 1.2 = 0.30000000000000004

 // 乘法
 0.8 * 3 = 2.4000000000000004
 19.9 * 100 = 1989.9999999999998

 // 除法
 0.3 / 0.1 = 2.9999999999999996
 0.69 / 10 = 0.06899999999999999

 // 比较
 0.1 + 0.2 === 0.3 // false
 (0.3 - 0.2) === (0.2 - 0.1) // false

二. 导致原因

JavaScript 内部只有一种数字类型Number,也就是说,JavaScript 语言的底层根本没有整数,所有数字都是以IEEE-754标准格式64位浮点数形式储存,1与1.0是相同的。因为有些小数以二进制表示位数是无穷的。JavaScript会把超出53位之后的二进制舍弃,所以涉及小数的比较和运算要特别小心。

三. IEEE二进制浮点数算术标准(IEEE 754)

IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。

四. 浮点数的存储

JS的浮点数实现也是遵循IEEE 754标准,采用双精度存储(double precision),使用64位固定长度来表示,其中1位用来表示符号位,11位用来表示指数,52位表示尾数。如下图:

JS浮点数运算结果不精确的Bug解决

  • ​符号位(sign):第1位是正负数符号位,0代表正数,1代表负数
  • 指数位(Exponent):中间11位存储指数,用来表示次方数
  • 尾数位(mantissa):最后的52位是尾数,超出部分自动进一舍零

五. 浮点数的计算步骤(0.1+0.2)

【1】首先,十进制的0.1和0.2会转换成二进制的,但是由于浮点数用二进制表示是无穷的

0.1——>0.0001 1001 1001 1001 ...(1001循环)
 0.2——>0.0011 0011 0011 0011 ...(0011循环)

【2】IEEE754标准的64位双精度浮点数的小数部分最多支持53位二进制,多余的二进制数字被截断,所以两者相加之后的二进制之和是

0.0100110011001100110011001100110011001100110011001101

【3】将截断之后的二进制数字再转换为十进制,就成了0.30000000000000004,所以在计算时产生了误差

六. 解决办法

【1】引用类库

  • Math.js 
  • decimal.js   
  • big.js

【2】思路一:在知道小数位个数的前提下,可以考虑通过将浮点数放大倍数到整型(最后再除以相应倍数),再进行运算操作,这样就能得到正确的结果了  

0.1 + 0.2 ——> (0.1 * 10 + 0.2 * 10) / 10 // 0.3
0.8 * 3 ——> ( 0.8 * 100 * 3) / 100         //2.4

【3】自定义一个转换和处理函数 

// f代表需要计算的表达式,digit代表小数位数
 Math.formatFloat = function (f, digit) {
  // Math.pow(指数,幂指数)
  var m = Math.pow(10, digit);
  // Math.round() 四舍五入
  return Math.round(f * m, 10) / m;
 }
 console.log(Math.formatFloat(0.3 * 8, 1)); // 2.4
 console.log(Math.formatFloat(0.35 * 8, 2)); // 2.8

【4】加法函数  

/**
  ** 加法函数,用来得到精确的加法结果
  ** 说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
  ** 调用:accAdd(arg1,arg2)
  ** 返回值:arg1加上arg2的精确结果
  **/
 function accAdd(arg1, arg2) {
  var r1, r2, m, c;
  try {
  r1 = arg1.toString().split(".")[1].length;
  } catch (e) {
  r1 = 0;
  }
  try {
  r2 = arg2.toString().split(".")[1].length;
  } catch (e) {
  r2 = 0;
  }
  c = Math.abs(r1 - r2);
  m = Math.pow(10, Math.max(r1, r2));
  if (c > 0) {
  var cm = Math.pow(10, c);
  if (r1 > r2) {
   arg1 = Number(arg1.toString().replace(".", ""));
   arg2 = Number(arg2.toString().replace(".", "")) * cm;
  } else {
   arg1 = Number(arg1.toString().replace(".", "")) * cm;
   arg2 = Number(arg2.toString().replace(".", ""));
  }
  } else {
  arg1 = Number(arg1.toString().replace(".", ""));
  arg2 = Number(arg2.toString().replace(".", ""));
  }
  return (arg1 + arg2) / m;
 }

 //给Number类型增加一个add方法,调用起来更加方便。
 Number.prototype.add = function (arg) {
  return accAdd(arg, this);
 };

【5】减法函数

/**
  ** 减法函数,用来得到精确的减法结果
  ** 说明:javascript的减法结果会有误差,在两个浮点数相减的时候会比较明显。这个函数返回较为精确的减法结果。
  ** 调用:accSub(arg1,arg2)
  ** 返回值:arg1加上arg2的精确结果
  **/
 function accSub(arg1, arg2) {
  var r1, r2, m, n;
  try {
  r1 = arg1.toString().split(".")[1].length;
  } catch (e) {
  r1 = 0;
  }
  try {
  r2 = arg2.toString().split(".")[1].length;
  } catch (e) {
  r2 = 0;
  }
  m = Math.pow(10, Math.max(r1, r2)); //last modify by deeka //动态控制精度长度
  n = (r1 >= r2) ? r1 : r2;
  return ((arg1 * m - arg2 * m) / m).toFixed(n);
 }

 // 给Number类型增加一个mul方法,调用起来更加方便。
 Number.prototype.sub = function (arg) {
  return accMul(arg, this);
 };

【6】乘法函数

/**
  ** 乘法函数,用来得到精确的乘法结果
  ** 说明:javascript的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。
  ** 调用:accMul(arg1,arg2)
  ** 返回值:arg1乘以 arg2的精确结果
  **/
 function accMul(arg1, arg2) {
  var m = 0,
  s1 = arg1.toString(),
  s2 = arg2.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类型增加一个mul方法,调用起来更加方便。
 Number.prototype.mul = function (arg) {
  return accMul(arg, this);
 };

【7】除法函数

/** 
   ** 除法函数,用来得到精确的除法结果
   ** 说明:javascript的除法结果会有误差,在两个浮点数相除的时候会比较明显。这个函数返回较为精确的除法结果。
   ** 调用:accDiv(arg1,arg2)
   ** 返回值:arg1除以arg2的精确结果
   **/
  function accDiv(arg1, arg2) {
   var t1 = 0,
    t2 = 0,
    r1, r2;
   try {
    t1 = arg1.toString().split(".")[1].length;
   } catch (e) {}
   try {
    t2 = arg2.toString().split(".")[1].length;
   } catch (e) {}
   with(Math) {
    r1 = Number(arg1.toString().replace(".", ""));
    r2 = Number(arg2.toString().replace(".", ""));
    return (r1 / r2) * pow(10, t2 - t1);
   }
  }

  //给Number类型增加一个div方法,调用起来更加方便。
  Number.prototype.div = function (arg) {
   return accDiv(this, arg);
  };

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
浏览器窗口加载和大小改变事件示例
Feb 27 Javascript
javascript操作表格排序实例分析
May 06 Javascript
浅析函数声明和函数表达式——函数声明的声明提前
May 03 Javascript
windows下vue-cli导入bootstrap样式
Apr 25 Javascript
Vuex 进阶之模块化组织详解
Jan 12 Javascript
vue.js系列中的vue-fontawesome使用
Feb 10 Javascript
vue异步axios获取的数据渲染到页面的方法
Aug 09 Javascript
详解在Node.js中发起HTTP请求的5种方法
Jan 10 Javascript
详解element-ui中form验证杂记
Mar 04 Javascript
vue路由守卫+登录态管理实例分析
May 21 Javascript
vue.config.js常用配置详解
Nov 14 Javascript
element-ui tooltip修改背景颜色和箭头颜色的实现
Dec 16 Javascript
微信小程序动画组件使用解析,类似vue,且更强大
Aug 01 #Javascript
vue项目中全局引入1个.scss文件的问题解决
Aug 01 #Javascript
jQuery中DOM操作原则实例分析
Aug 01 #jQuery
详解package.json版本号规则
Aug 01 #Javascript
ES6 新增的创建数组的方法(小结)
Aug 01 #Javascript
详解基于Wepy开发小程序插件(推荐)
Aug 01 #Javascript
深入浅析Vue中mixin和extend的区别和使用场景
Aug 01 #Javascript
You might like
浅析Apache中RewriteCond规则参数的详细介绍
2013/06/30 PHP
php上传图片到指定位置路径保存到数据库的具体实现
2013/12/30 PHP
PHP编程获取音频文件时长的方法【基于getid3类】
2017/04/20 PHP
微信封装的调用微信签名包的类库
2017/06/08 PHP
Mootools 1.2教程 滚动条(Slider)
2009/09/15 Javascript
Javascript中的delete介绍
2012/09/02 Javascript
JS实现一个列表中包含上移下移删除等功能
2014/09/24 Javascript
JavaScript实现鼠标滑过图片变换效果的方法
2015/04/16 Javascript
ztree获取选中节点时不能进入可视区域出现BUG如何解决
2015/12/03 Javascript
JS中多种方式创建对象详解
2016/03/22 Javascript
简单的JS轮播图代码
2016/07/18 Javascript
JavaScript定义数组的三种方法(new Array(),new Array('x','y')
2016/10/04 Javascript
javascript表单正则应用
2017/02/04 Javascript
微信小程序本作用域下调用全局JS详解及实例
2017/02/22 Javascript
Vue+Vux项目实践完整代码
2017/11/30 Javascript
详解VueJS应用中管理用户权限
2018/02/02 Javascript
vue-router权限控制(简单方式)
2018/10/29 Javascript
在 Angular-cli 中使用 simple-mock 实现前端开发 API Mock 接口数据模拟功能的方法
2018/11/28 Javascript
nodejs中方法和模块用法示例
2018/12/24 NodeJs
vue 中 命名视图的用法实例详解
2019/08/14 Javascript
Node.JS用纯JavaScript生成图片或滑块式验证码功能
2019/09/12 Javascript
[03:11]不朽宝藏三外观展示
2020/09/18 DOTA
[49:12]完美世界DOTA2联赛PWL S2 Magma vs GXR 第二场 11.29
2020/12/02 DOTA
轻松实现python搭建微信公众平台
2016/02/16 Python
Django自定义用户认证示例详解
2018/03/14 Python
如何将PySpark导入Python的放实现(2种)
2020/04/26 Python
python中tab键是什么意思
2020/06/18 Python
CSS3的颜色渐变效果的示例代码
2017/09/29 HTML / CSS
韩国11街:11STREET
2018/03/27 全球购物
新加坡网上美容店:Hermo新加坡
2019/06/19 全球购物
西班牙在线药店:DosFarma
2020/03/28 全球购物
个人自我鉴定
2013/11/07 职场文书
小学教师管理制度
2014/01/18 职场文书
网络技术专业推荐信
2014/02/20 职场文书
公共场所标语
2014/06/30 职场文书
div与span之间的区别与使用介绍
2021/12/06 HTML / CSS