首页 > 编程语言 >深入探讨JavaScript中的精度问题:原理与解决方案

深入探讨JavaScript中的精度问题:原理与解决方案

时间:2024-10-10 12:53:03浏览次数:3  
标签:二进制 解决方案 浮点数 JavaScript 深入探讨 整数 十进制 精度 小数

深入探讨JavaScript中的精度问题:原理与解决方案

在日常的JavaScript开发中,我们经常会遇到一些令人困惑的数值计算问题,特别是涉及到小数点运算时。例如,为什么0.1 + 0.2的结果不是预期的0.3,而是0.30000000000000004?本文将详细介绍JavaScript中出现精度问题的原因,深入解析十进制小数如何存储为二进制,以及如何避免和解决精度问题。

一、JavaScript中的精度问题现象

让我们先来看几个实际的例子:

console.log(0.1 + 0.2);          // 输出:0.30000000000000004
console.log(0.1 + 0.7);          // 输出:0.7999999999999999
console.log(0.2 * 0.4);          // 输出:0.08000000000000002
console.log(0.3 / 0.1);          // 输出:2.9999999999999996

这些结果可能出乎我们的意料,但在JavaScript中却是常见的。这些精度问题主要发生在浮点数运算中。

二、精度问题的底层原因

1. IEEE 754 双精度浮点数标准

JavaScript中的数字(Number类型)遵循IEEE 754标准,使用64位双精度浮点数表示。这种表示方法在计算机中非常常见,但也带来了浮点数精度的问题。

2. 十进制小数如何存储为二进制

2.1 整数部分的二进制表示

对于整数部分,将十进制整数不断除以2,取余数,逆序排列即可得到二进制表示。

示例:将十进制数5转换为二进制。

5 ÷ 2 = 2 ...... 1
2 ÷ 2 = 1 ...... 0
1 ÷ 2 = 0 ...... 1

逆序排列余数:1 0 1

因此,5的二进制表示为101
2.2 小数部分的二进制表示

小数部分的转换需要将十进制小数不断乘以2,取其整数部分(0或1),直到小数部分为0或达到所需的精度。

示例:将十进制小数0.625转换为二进制。

0.625 × 2 = 1.25     整数部分:1
0.25  × 2 = 0.5      整数部分:0
0.5   × 2 = 1.0      整数部分:1

因此,0.625的二进制表示为0.101
2.3 不能精确表示的十进制小数

然而,对于某些十进制小数,如0.10.2等,在二进制中是无限循环小数,无法精确表示。

示例:将十进制小数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.4 × 2 = 0.8        整数部分:0
...

这个过程会无限循环,得到的二进制小数是:

0.0001100110011001100110011001100110011001100110011...
2.4 二进制浮点数的表示

在IEEE 754标准中,浮点数表示为:

(-1)^符号位 × 1.尾数 × 2^(指数位)

64位双精度浮点数的结构

  • 1位符号位(S):0表示正数,1表示负数。
  • 11位指数位(E):存储指数的偏移值。
  • 52位尾数(M):又称为有效数字或小数部分。

由于尾数只有有限的52位,无法存储无限循环的小数,必须进行截断或舍入,导致精度损失。

3. 二进制无法精确表示某些十进制小数

因为某些十进制小数在二进制中是无限循环的,所以在存储这些小数时,只能保留有限的位数,导致精度损失。

示例

  • 十进制0.1的二进制近似值:
    0.0001100110011001100110011001100110011001100110011001101(共52位尾数)
    
  • 但实际的二进制表示只能保留52位尾数,超出的部分被截断。

4. 浮点数的舍入误差

由于截断或舍入的存在,浮点数在存储时会引入微小的误差。当对这些浮点数进行运算时,误差会被放大或累积,导致最终结果与预期不符。

5. 浮点数的计算过程

0.1 + 0.2为例:

  1. 将十进制小数转换为二进制浮点数

    • 0.1的二进制近似值:
      0.0001100110011001100110011001100110011001100110011001101
      
    • 0.2的二进制近似值:
      0.001100110011001100110011001100110011001100110011001101
      
  2. 进行二进制加法

    将上述二进制数相加,得到结果(仍然是近似值)。

  3. 将二进制结果转换回十进制

    得到的十进制结果为0.30000000000000004,出现了精度误差。

三、扩展到其他进制小数的表示

1. 八进制和十六进制

与二进制类似,八进制和十六进制也无法精确表示某些十进制小数。这是因为进制之间的基数差异,导致在转换过程中出现无限循环小数。

1.1 八进制小数
  • 八进制的基数是8,小数部分的每一位表示(1/8)^n的倍数。
  • 某些十进制小数在八进制中会出现无限循环小数。

示例:将十进制小数0.3转换为八进制。

0.3 × 8 = 2.4      整数部分:2
0.4 × 8 = 3.2      整数部分:3
0.2 × 8 = 1.6      整数部分:1
0.6 × 8 = 4.8      整数部分:4
0.8 × 8 = 6.4      整数部分:6
0.4 × 8 = 3.2      整数部分:3
...

得到八进制小数:0.231463...

#### 1.2 十六进制小数

- **十六进制的基数是16**,小数部分的每一位表示`(1/16)^n`的倍数。
- 同样地,某些十进制小数在十六进制中无法精确表示。

**示例**:将十进制小数`0.1`转换为十六进制。

0.1 × 16 = 1.6 整数部分:1
0.6 × 16 = 9.6 整数部分:9
0.6 × 16 = 9.6 整数部分:9

得到十六进制小数:0.1999…(无限循环)


### 2. 任意进制之间的小数转换

小数在不同进制之间的转换,本质上是基数的不同。由于某些进制之间的基数无法整除,导致小数在转换时会出现无限循环的情况。

#### 2.1 常见的无法精确表示的情况

- **二进制无法精确表示十分之一(0.1)**
- **十进制无法精确表示三分之一(0.(3))**
- **八进制无法精确表示某些十进制小数**

#### 2.2 循环小数的概念

在某个进制下,小数部分无限循环的数称为循环小数。例如,在十进制中,`1/3 = 0.(3)`,表示`0.3333...`无限循环。

## 四、如何避免和解决精度问题

### 1. 使用整数进行计算(定点数运算)

将浮点数转换为整数,进行运算后再转换回浮点数。例如,将金额以最小单位(如“分”)存储和计算。

```javascript
let a = 0.1;
let b = 0.2;
let result = (a * 100 + b * 100) / 100;
console.log(result); // 输出:0.3

注意:这种方法适用于有限的小数位数,且需要谨慎处理除法运算。

2. 使用toFixed()方法

toFixed()方法可以指定小数点后的位数,并返回一个字符串。

let result = (0.1 + 0.2).toFixed(1);
console.log(result); // 输出:"0.3"

缺点toFixed()返回的是字符串,可能需要转换为数字。此外,toFixed()也有可能出现四舍五入误差。

3. 引入精度校正函数

编写一个函数,对浮点数运算进行精度校正。

function add(a, b) {
    let r1 = 0, r2 = 0, m;
    try { r1 = a.toString().split(".")[1].length } catch (e) {}
    try { r2 = b.toString().split(".")[1].length } catch (e) {}
    m = Math.pow(10, Math.max(r1, r2));
    return (a * m + b * m) / m;
}

console.log(add(0.1, 0.2)); // 输出:0.3

解释:通过计算小数位数,转换为整数进行运算,然后再转换回浮点数。

4. 使用第三方精度计算库

使用成熟的第三方库,可以方便地进行高精度的数值计算。

  • Decimal.js

    const Decimal = require('decimal.js');
    let result = new Decimal(0.1).plus(0.2);
    console.log(result.toString()); // 输出:"0.3"
    
  • BigNumber.js

    const BigNumber = require('bignumber.js');
    let result = new BigNumber(0.1).plus(0.2);
    console.log(result.toString()); // 输出:"0.3"
    

优点:支持任意精度的数值计算,避免了JavaScript原生的浮点数精度问题。

缺点:需要引入外部库,增加了项目的依赖。

5. ES2020中的BigInt

ES2020引入了BigInt类型,用于表示任意精度的整数。不过它只能处理整数,不能直接解决小数的精度问题。

let bigIntNum = BigInt("9007199254740993");
console.log(bigIntNum); // 输出:9007199254740993n

注意BigInt不能与Number类型直接混合运算,需要进行类型转换。

五、实际应用中的注意事项

1. 金融计算

在涉及金额的计算中,必须特别小心精度问题。通常的做法是:

  • 将金额以最小单位(如“分”)存储和计算。
  • 使用高精度的计算库。
  • 避免直接使用浮点数进行金额计算。

2. 比较浮点数

在比较两个浮点数是否相等时,不要直接使用=====,而是判断它们的差值是否在一个很小的范围内。

function numbersAlmostEqual(a, b, epsilon = Number.EPSILON) {
    return Math.abs(a - b) < epsilon;
}

console.log(numbersAlmostEqual(0.1 + 0.2, 0.3)); // 输出:true

解释Number.EPSILON是JavaScript中能够表示的最小的正数,使得1 + Number.EPSILON !== 1

3. 避免累积误差

在大量的浮点数运算中,微小的误差会累积,导致结果偏差较大。可以考虑:

  • 将计算过程中的中间结果进行精度校正。
  • 使用精度更高的数据类型或计算方法。

六、总结

关键点:

  • 十进制小数在二进制中可能无法精确表示,导致舍入误差。
  • 浮点数的有限位数表示,限制了存储精度。
  • 在不同进制之间,小数的转换可能导致无限循环小数,这是进制间转换的固有问题。

解决方案:

  • 使用整数替代:将小数转换为整数进行计算,避免浮点数精度问题。
  • 使用精度校正函数:在运算过程中进行精度调整,减少误差。
  • 使用高精度计算库:引入第三方库,如Decimal.jsBigNumber.js,实现任意精度的数值计算。
  • 在比较浮点数时,使用容差范围:判断两个浮点数的差值是否在可接受的范围内,而不是直接比较相等。

通过深入理解JavaScript中的精度问题,以及十进制小数如何存储为二进制,我们可以编写出更加健壮和可靠的代码,提升程序的准确性和稳定性。

参考资料:

标签:二进制,解决方案,浮点数,JavaScript,深入探讨,整数,十进制,精度,小数
From: https://blog.csdn.net/qq_33546823/article/details/142743197

相关文章

  • JavaScript Number研究03_实例方法_toExponential_toFixed_toPrecision_toString_valu
    JavaScriptNumber研究03:实例方法——toExponential、toFixed、toPrecision、toString、valueOf、toLocaleString在JavaScript中,Number对象不仅包含了许多有用的静态属性,还提供了一系列实例方法,帮助我们在不同场景下处理和转换数值。这些方法包括:toExponential()toFixed()......
  • 工控网络损伤问题的解决方案
       工控网络的可靠性和健壮性对于确保系统的无故障运行至关重要,因此网络可靠性的验证必不可少。线路上存在损伤或异常时的系统表现真正的决定了被测系统的可靠性等级。    如果能够提前预知网络系统在某些特定异常存在的情况下的性能表现,就可以在真实的应用环境中快......
  • 现网性能测试解决方案
    在一些现网环境中,网络设备对于实际业务流量的转发性能往往不易获得    针对网络性能,传统的测试方案一般使用网络测试仪向被测网络发送测试流量,经被测网络转发回测试仪生成各项性能数据的统计。但在实际应用中,一些特殊业务报文在网络中的转发性能取决于协议本身的交互过程,对......
  • 【JavaScript实用日期星期函数】日期格式化、获取日期是星期几、今后7天的日期、本周
    ......
  • 自动化测试 | 安装selenium教程以及(ERROR: pip‘s dependency resolver does not cur
    1.在cmd里面安装selenium输入:pipinstallselenium在我这里出现了下载缓慢的问题,一直卡了半天,有同样问题的小伙伴可以试试输入下面这个进行安装,会更快一点:pip--default-timeout=100installselenium-ihttps://pypi.tuna.tsinghua.edu.cn/simple之后仍然出现了:ERRO......
  • JavaScript中的运算符
    一、运算符分类运算符:(operator):也称之为叫操作符。是用来实现赋值、比较和执行运算等功能的符号javaScript中常用的运算符:算数运算符递增和递减运算符比较运算符逻辑运算符赋值运算符1.算数运算符概述:算数运算使用的符号,用于执行两个变量或值得算数运算运算符描述......
  • 打通前后端流程,案例解读华为云开源低代码引擎解决方案
    本文分享自华为云社区《使用场景级前端解决方案及低代码引擎,助力开发者生产效能提升》,来源:《华为云DTSE》第五期开源专刊当前前端在场景级的前端能力/物料上,业界相关内容/产品较少,并且较分散,基本无基础组件搭配,体验参差,多数专业场景能力/物料仅商用授权;对于场景的构建,开发效率与......
  • [Javascript] Using defineProperty to observe the object props changes
    constobj={a:1,b:2,c:{a:1,b:2,},};functionisObject(val){returnval!==null&&typeofval==="object";}functionobserve(obj){for(letkeyinobj){letv=obj[key];if(isObject(v)){......
  • 【javascript 编程】Web前端之JavaScript动态添加类名的两种方法、区别、className、c
    通过className来添加或删除类名添加类名获取元素el.className="类名1类名2...";多个类名用空格隔开。移除类名获取元素名el.className="";直接等于一个空字符串即可删除类名。通过classList来添加或删除类名添加一个类名获取元素名el.classList.add("类名");。......
  • [Javascript] Using Proxy to observe the object
    constobj={a:1,b:2,c:{d:1,e:2,},};functionisObject(val){returnval!==null&&typeofval==="object";}functionobserve(obj){constproxy=newProxy(obj,{get(target,key){constv......