JS中数学计算精度问题的解决方案

 更新时间:2023年12月03日 09:01:29   作者:凡铁  
这篇文章主要给大家介绍了JS中数学计算精度问题的解决方案,文中通过代码示例和图文结合给大家讲解非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下

故事从0.1+0.2说起

0.1+0.2是否等于0.3呢?

这是一个前端人耳熟能详的故事,每一个初入前端世界的人,应该都会被它来一次灵魂拷问。它的出现,似乎打破了人们以往对于代码世界“执行严谨、一丝不苟”的刻板印象。然而,这看起来“不够严谨”的形成原因,却正是因为底层代码执行的足够严谨

在初入前端世界的时候,有那么一瞬,我甚至在想难道是底层对于0.10.2有一种特殊的感情?

然而事实并非如此,能被底层这种庞然大物看上并针对的,当然不会只有0.10.2这两个看起来平平无奇的数字,而是包含了这两个数字在内的一批特殊存在。

如下图所示:

当然,除上述图片内的数字之外,还有更多的其他数字也在这反常理的队列之内。

然而,这篇文章我们并不是来深入讨论这些特殊的数字在进行数学计算时,与底层究竟产生了什么样的恩怨纠葛。我们只需要简单知道:在计算机世界中,所有信息最后都是以二进制存储的,可是数字中的小数部分在按照一定规则转换为二进制时,有些数字会产生无限循环的现象,但计算机精度位数是有限的,所以对超出位数的部分做了四舍五入的计算,因此造成了精度的丢失。

本文仅仅针对以上现象,结合日常开发的实践,讨论一些解决问题的方法。

初步解决

既然是因为小数部分在转换二进制时做了四舍五入的处理,那么计算时先将小数转为整数再计算是不是就可以了?

依据上面的思想,在javascript中进行小数计算,通常会采用放大倍数取整之后再计算,得出结果之后再缩小还原的技术方案。

例:计算0.1+0.2,通常会将0.1和0.2放大10倍,相加之后再缩小10倍

代码如下:

  /**
   * 已知:a为0.1,b为0.2
   * 求:a与b的和
   * */
  const a = 0.1;
  const b = 0.2;
  const result = ((a * 10) + (b * 10)) / 10;
  console.log(result) // 0.3

如上,针对已知小数位数的数字,我们可以直接采用放大相应倍数取整,然后再计算的方式来规避小数计算的精度问题。

可是在实际的业务开发中,对于需要进行计算处理的数字,我们往往无法预先获知数字包含的小数位数。对于此种情况,我们便需要先确定小数位数,然后确定放大倍数,再进行计算。

代码如下:

  /**
   * 已知:a,b为两个精度随机的小数
   * 求:a与b的和
  */

  // 生成精度随机的小数
  const getNumber = () => {
    const len = Math.random() * 10;
    const num = Number((Math.random() * 10).toFixed(len))
    return num
  }

  // 计算放大倍数
  const getPower = (a, b) => {
    // 获取a,b小数位长度,如没有小数位则默认值为0
    const aLen = a.toString().split(".")[1]?.length || 0;
    const bLen = b.toString().split(".")[1]?.length || 0;
    // 获取最大长度
    const len = Math.max(aLen, bLen);
    // 计算返回放大倍数
    return Math.pow(10, len)
  }

  const a = getNumber();
  const b = getNumber();
  const power = getPower(a, b);

  const result = ((a * power) + (b * power)) / power;

因为以上代码中,ab皆由getNumber函数随机生成,为了便于观察,我们添加log后,在浏览器中运行代码。

如下图所示:

观察可知,计算结果正确。以上,我们通过使用getPower函数确定放大的倍数,然后进行计算。这也是目前大部分同学解决小数计算精度问题的主要方式。

然而,故事到这里就结束了吗?

当然不是!

以上方式虽然解决了一些精度问题,但是并没有解决所有的精度问题。在这个特殊的小数群体中,并不是所有的小数都可以通过放大倍数来取整的!

如下图所示:

因此先放大再计算也并不是十分可靠,如下图所示:

大胆取舍

Number.EPSILON

我们已经知道,精度的误差是由于底层在计算时做了一些四舍五入造成的,因此我们分析后可以断定被舍弃的部分一定是小于可以表示的最小浮点数的。

例如:有数字 a1.234,对 a 做保留两位小数的处理后,得到数字 bb的值为1.23。则上述操作中舍弃的部分 0.004,一定小于保留精度 0.01

基于以上分析,我们有理由相信:在 javascript 中当两个数字之间的差值小于可以表示的最小浮点数,那么我们就认为这两个数字相等。

可是,最小的浮点数该如何获取呢?

javascript 为我们提供了这样一个属性:Number.EPSILON 静态数据属性,表示 1 与大于 1 的最小浮点数之间的差值。

详细介绍可查看MDN

我们对之前的代码做一些优化,在放大一定倍数之后,做差值比较,确认最终结果。

代码如下:

const getPower = (a, b, c) => {
  // 获取a,b小数位长度,如没有小数位则默认值为0
  const aLen = a.toString().split(".")[1]?.length || 0;
  const bLen = b.toString().split(".")[1]?.length || 0;
  const cLen = c.toString().split(".")[1]?.length || 0;
  // 获取最大长度
  const len = Math.max(aLen, bLen, cLen);
  // 计算返回放大倍数
  return Math.pow(10, len)
}
// 差值比价
const compare = (n) => {
  const result = Math.round(n);

  // 如差值小于 Number.EPSILON 则认为和取整之后的数字相等
  return n - result < Number.EPSILON ? result : n;
}

var a = 19.9;
var b = 4788.4;
var c = 0.01;
var power = getPower(a, b, c);

const result = (compare((a * power)) + compare((b * power)) + compare((c * power))) / power
console.log(result)

在浏览器运行代码,可知计算正确。

如下图所示:

Math.round

理论上讲,一个两位小数乘 100 后一定会得到一个整数,一个三位小数乘 1000 以后一定也会得到一个整数。同理可知,一个 n 位小数乘(10^n)后,一定可以得到一个整数!

虽然在计算机世界中小数计算有些误差,但通过上述代码我们知道,这个误差小到几乎可以忽略,那么我们是不是可以大胆一点,放大之后无需比较,直接四舍五入!

我们修改以上代码,舍弃compare函数。

代码如下:

const getPower = (a, b, c) => {
  // 获取a,b小数位长度,如没有小数位则默认值为0
  const aLen = a.toString().split(".")[1]?.length || 0;
  const bLen = b.toString().split(".")[1]?.length || 0;
  const cLen = c.toString().split(".")[1]?.length || 0;
  // 获取最大长度
  const len = Math.max(aLen, bLen, cLen);
  // 计算返回放大倍数
  return Math.pow(10, len)
}

var a = 19.9;
var b = 4788.4;
var c = 0.01;
var power = getPower(a, b, c);

const result = (Math.round((a * power)) + Math.round((b * power)) + Math.round((c * power))) / power
console.log(result)

在浏览器中运行后发现,结果依然正确。

如下图所示:

封装完善

基于以上推导,我们可以封装一个简易的计算函数。

代码如下:

function compute(type, ...args) {
  // 计算放大倍数
  const getPower = (numbers) => {
    const lens = numbers.map(num => num.toString().split(".")[1]?.length || 0);
    // 获取最大长度
    const len = Math.max(...lens);
    // 计算返回放大倍数
    return Math.pow(10, len)
  }

  // 获取放大倍数
  const power = getPower(args);

  // 获取放大后的值
  const newNumbers = args.map(num => Math.round(num * power));

  // 计算结果
  let result = 0;
  switch (type) {
    case "+":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber + nextNumber, result) / power;
      break;
    case "-":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber - nextNumber) / power;
      break;
    case "*":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber * nextNumber) / (power ** newNumbers.length);
      break;
    case "/":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber / nextNumber);
      break;
  }

  return {
    result,
    next(nextType, ...nextArgs) {
      return compute(nextType, result, ...nextArgs);
    }
  }
}

// 验证
const arr = [0.1, 0.2, 29.6]
const a = compute('+', ...arr);
const b = a.next('-', 4, 2, 4);
const c = b.next('*', 100);
const d = c.next('+', 2798.4);
const e = d.next('*', 100);
const f = e.next('/', 1000);
const r = compute('+', ...arr).next('-', 4, 2, 4).next('*', 100).next('+', 2798.4).next('*', 100).next('/', 1000);

console.log('a: ', a.result) // a:  29.9
console.log('b: ', b.result) // b:  19.9
console.log('c: ', c.result) // c:  1990
console.log('d: ', d.result) // d:  4788.4
console.log('e: ', e.result) // e:  478840
console.log('f: ', f.result) // f:  478.84
console.log('r: ', r.result) // f:  478.84

经简单测试后,可知compute函数已实现基本的四则运算,且可以链式调用。

结语

若有错误,请务必给予指正。 谢谢!

以上就是JS中数学计算精度问题的解决方案的详细内容,更多关于JS数学计算精度问题的资料请关注脚本之家其它相关文章!

相关文章

  • JavaScript实现飞机大战游戏

    JavaScript实现飞机大战游戏

    这篇文章主要为大家详细介绍了JavaScript实现飞机大战游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09
  • JS继承实现方法及优缺点详解

    JS继承实现方法及优缺点详解

    这篇文章主要介绍了JS继承实现方法及优缺点详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • JavaScript中的动态 import()用法示例解析

    JavaScript中的动态 import()用法示例解析

    这篇文章主要为大家介绍了JavaScript中的动态import()用法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • BootStrap实现鼠标悬停下拉列表功能

    BootStrap实现鼠标悬停下拉列表功能

    这篇文章主要介绍了BootStrap实现鼠标悬停下拉列表功能,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-02-02
  • .html文件防止script脚本缓存的三种方法

    .html文件防止script脚本缓存的三种方法

    现在有这样一个问题,由于一些原因,我们经常需要修改gTool的代码然后上传更新cdn,但是用户通过 html 文件访问的 gTool 链接实际上还是缓存的,所以现在问题就是如何禁止缓存,文中给大家介绍了三个方法供大家参考,需要的朋友可以参考下
    2024-01-01
  • 解决WordPress使用CDN后博文无法评论的错误

    解决WordPress使用CDN后博文无法评论的错误

    这篇文章主要介绍了解决WordPress使用CDN后博文无法评论的错误的方法,同时提醒注意WordPress使用版本的jQuery版本支持度,需要的朋友可以参考下
    2015-12-12
  • JavaScript变量作用域_动力节点Java学院整理

    JavaScript变量作用域_动力节点Java学院整理

    这篇文章主要介绍了JavaScript变量作用域,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • JS判断字符串字节数并截取长度的方法

    JS判断字符串字节数并截取长度的方法

    这篇文章主要介绍了JS判断字符串字节数并截取长度的方法,涉及JavaScript针对页面元素与字符串的动态操作技巧,需要的朋友可以参考下
    2016-03-03
  • 如何在JavaScript 中获取域名

    如何在JavaScript 中获取域名

    本文将讨论如何使用 JavaScript 事件和函数在网页执行期间以编程方式获取域名,本文结合示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-06-06
  • echarts柱状堆叠图实现示例(图例和x轴都是动态的)

    echarts柱状堆叠图实现示例(图例和x轴都是动态的)

    一些柱形图在数据量比较多的时候,横向排列受到挤压,导致柱形图,变的非常细,影响整体的效果,下面这篇文章主要给大家介绍了关于echarts柱状堆叠图(图例和x轴都是动态的)的相关资料,需要的朋友可以参考下
    2023-04-04

最新评论