JavaScript浮点数精度问题原理与解决方案

作者:闭包旅人时间:2026-10-24 01:33:08浏览:307

本文深入解析JavaScript中浮点数运算产生误差的根本原因,即遵循IEEE 754标准的二进制存储机制。通过0.1+0.2的经典案例演示精度丢失过程,并提供引用类库、放大倍数法及自定义加减乘除函数等实用解决方案,适用于对精度要求严格的财务系统开发。

JavaScript浮点数精度问题原理与解决方案

前言

近期在项目开发中,涉及产品价格的计算时,频繁遭遇JavaScript浮点数精度异常。对于财务管理系统的开发者而言,这是一个极其严重的问题,因为任何涉及资金的计算都容不得差错。本文将梳理相关成因及解决方案,供各位开发者参考。

常见例子

一. 常见例子


 // 加法
 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,这意味着其底层并不区分整数与浮点数,所有数值均以IEEE-754标准定义的64位浮点数格式存储,因此整数1与浮点数1.0在底层是相同的。由于部分小数转换为二进制时位数无限,JavaScript会舍弃超过53位后的二进制位,导致涉及小数的比较与运算时必须格外谨慎。

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

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

IEEE二进制浮点数算术标准(IEEE 754)自20世纪80年代以来成为最广泛采用的浮点数运算规范,被众多CPU及浮点运算器所支持。该标准定义了浮点数的表示格式(含负零-0)与反常值(denormal number),规定了特殊数值(无穷大Inf与非数值NaN)及其“浮点数运算符”,并明确了四种数值舍入规则及五种异常状况(包括发生时机与处理方式)。

浮点数的存储

四. 浮点数的存储

JavaScript的浮点数实现同样遵循IEEE 754标准,采用双精度(double precision)存储,使用固定的64位长度表示。其中,1位用于表示符号位,11位用于表示指数,剩余的52位用于表示尾数。具体结构如下图所示:

JavaScript浮点数精度问题原理与解决方案

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

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

五. 浮点数的计算步骤(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);
  };

总结

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对本站的支持。

作者最新文章