深入探讨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中的精度问题,以及十进制小数如何存储为二进制,我们可以编写出更加健壮和可靠的代码,提升程序的准确性和稳定性。

参考资料:

  • MDN文档 - Number
  • IEEE 754标准
  • 浮点数精度问题详解
  • BigNumber.js库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/55776.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Laravel Filament 如何配置多语言支持

演示 一、安装拓展包outerweb/filament-translatable-fields composer require outerweb/filament-translatable-fields配置模型 该套件包含一个名为 HasTranslations 的特性&#xff0c;用于使 Eloquent 模型具备多语言功能。翻译值以 JSON 格式存储&#xff0c;并不需要额外…

Run the FPGA VI 选项的作用

Run the FPGA VI 选项的作用是决定当主机 VI 运行时&#xff0c;FPGA VI 是否会自动运行。 具体作用&#xff1a; 勾选 “Run the FPGA VI”&#xff1a; 当主机 VI 执行时&#xff0c;如果 FPGA VI 没有正在运行&#xff0c;系统将自动启动并运行该 FPGA VI。 这可以确保 FPG…

夜间数据库IO负载飙升?MySQL批量删除操作引发的问题排查

目录 问题现象 问题分析 修改建议 总结 问题现象 近日&#xff0c;某用户反馈他们的MySQL数据库实例在凌晨时段会频繁出现IO负载急剧上升的情况&#xff0c;这种状态会持续一段时间&#xff0c;随后自行恢复正常。为了查明原因&#xff0c;该用户通过DBdoctor工具收集了相…

js进阶——深入解析JavaScript中的URLSearchParams

深入解析 JavaScript 中的 URLSearchParams 在现代Web开发中&#xff0c;我们经常需要处理URL中的查询参数&#xff0c;尤其是在构建动态Web应用时。这些查询参数&#xff08;query parameters&#xff09;通常以 ?keyvalue&key2value2 的形式存在。JavaScript 提供了一个…

javascript:void(0)

javascript:void(0)是一种常用于HTML中的Javascript语句&#xff0c;通常用作链接的href属性。它的主要作用是防止链接的默认行为(例如跳转到一个新页面或刷新当前页面)&#xff0c;同时又可以执行一些Javascript代码 详细解释 javascript&#xff1a;这是一个协议&#xff0c…

第十三章 Redis短信登录实战(基于Redis)

目录 一、概述 1.1. Session复制 1.2. 使用Redis 二、基于Redis实现共享Session登录 2.1. 实现思路 2.2. 功能实现的主要代码 2.2.1. 用户业务接口 2.2.2. 用户业务接口实现类 2.2.3. 用户控制层 2.2.4. 登录拦截器 2.2.5. 拦截器配置类 2.3. 优化登录拦截器 完…

【PostgreSQL】实战篇——数据备份和恢复的最佳实践和工具

数据备份和恢复是确保数据安全性和可用性的关键环节。无论是由于硬件故障、软件错误、数据损坏还是人为错误&#xff0c;能够快速恢复数据都是保护业务连续性的重要措施。 PostgreSQL 提供了多种备份和恢复工具和方法&#xff0c;其中最常用的包括 pg_dump、pg_restore 和点时…

C++、Ruby和JavaScript

C C最初被称为带类的C, 兼容C的语法&#xff0c;此既是C得以流行的前提&#xff0c;也是C某些语法被捆绑的根源。C的来源于C语言的递增运算符&#xff0c;代表增加&#xff0c;意义为扩展。 C的历史 C类的设计思想来源于Simula. Simula为模拟的意思&#xff0c;被称为最早的面向…

电池大师 2.3.9 | 专业电池管理,延长寿命优化性能

Battery Guru 显示电池使用情况信息&#xff0c;测量电池容量&#xff08;mAh&#xff09;&#xff0c;并通过有用技巧帮助用户改变充电习惯&#xff0c;延长电池寿命。支持显示电池健康状况&#xff0c;优化电池性能。 大小&#xff1a;9.6M 百度网盘&#xff1a;https://pan…

26.删除有序数组中的重复项

题目::26. 删除有序数组中的重复项 - 力扣&#xff08;LeetCode&#xff09; 思路:只要不和前面的数一样就可以移动指针&#xff0c;进行赋值 代码: class Solution { public:int removeDuplicates(vector<int>& nums) {int slow 0 ;for(int fast 1; fast < …

髓质脊髓三叉神经核文献阅读笔记

文献阅读 1.RNA-seq 对于大量RNA测序&#xff0c;收集第30天的类器官。使用FastPure细胞/组织总RNA分离试剂盒根据制造商的方案提取总RNA。采用Nanodrop 2000分光光度计测定RNA浓度和纯度。使用Agilent 2100生物分析仪和2100 RNA纳米6000检测试剂盒评估RNA样品的完整性。简单…

选型工单管理系统,从原理到应用全面解读

工单管理系统提升客户支持效率&#xff0c;优化内部协作&#xff0c;强化数据分析。选型需明确需求&#xff0c;比较系统功能和特性&#xff0c;评估试用后选择最适合的系统。ZohoDesk凭其多渠道支持、智能分配、自动化工具、协作工具和数据分析能力&#xff0c;成为企业优选。…

Redis篇(缓存机制 - 基本介绍)(持续更新迭代)

目录 一、缓存介绍 二、经典三缓存问题 1. 缓存穿透 1.1. 简介 1.2. 解决方案 1.3. 总结 2. 缓存雪崩 2.1. 简介 2.2. 解决方案 2.3. 总结 3. 缓存击穿 3.1. 简介 3.2. 解决方案 3.3. 总结 4. 经典三缓存问题出现的根本原因 三、常见双缓存方案 1. 缓存预热 1…

MySQL进阶 - 索引

01 索引概述 【1】概念&#xff1a;索引就是一种有序的数据结构&#xff0c;可用于高效查询数据。在数据库表中除了要保存原始数据外&#xff0c;数据库还需要去维护索引这种数据结构&#xff0c;通过这种数据结构来指向原始数据&#xff0c;这样就可以根据这些数据结构实现高…

一个月学会Java 第8天 方法与递归

Day8 方法与递归 方法这个东西我们之前讲过&#xff0c;但是只是讲了原理并没有详细的讲解东西&#xff0c;还有构造器这个东西&#xff0c;也只是介绍过全貌&#xff0c;构造器其实就是一个特殊的方法&#xff0c;但是由于特殊&#xff0c;所以我们之后再讲&#xff0c;还有一…

Android kotlin密封类-基本使用

定义: 密封类&#xff08;Sealed Classes&#xff09;是一种特殊的类&#xff0c;它可以有一组受限的子类。与普通的基类不同&#xff0c;密封类的所有子类都必须在与密封类相同的文件中声明。这种限制使得密封类非常适合于表示固定的类层次结构&#xff0c;特别是在使用 when…

yolov8/9/10/11模型在中医舌苔分类识别中的应用【代码+数据集+python环境+GUI系统】

yolov8、9、10、11模型在中医舌苔分类识别中的应用【代码数据集python环境GUI系统】 背景意义 目前随着人们生活水平的不断提高&#xff0c;对于中医主张的理念越来越认可&#xff0c;对中医的需求也越来越多。 传统中医的舌诊主要依赖于医生的肉眼观察&#xff0c;仅仅通过这…

【AI知识点】模型对齐(Model Alignment)

更多AI知识点总结见我的专栏&#xff1a;【AI知识点】 AI论文精读、项目和一些个人思考见我另一专栏&#xff1a;【AI修炼之路】 有什么问题、批评和建议都非常欢迎交流&#xff0c;三人行必有我师焉&#x1f601; 模型对齐&#xff08;Model Alignment&#xff09; 是在人工智…

69.【C语言】动态内存管理(重点)(2)

本文为数据结构打下基础 备注:数据结构需要掌握指针,结构体和动态内存管理 承接68.【C语言】动态内存管理(重点)(1)文章 目录 3.free函数 cplusplus网的翻译 提炼要点 使用 x86debug环境下, 打开内存窗口 建议 3.free函数 cplusplus的介绍 点我跳转 cplusplus网的翻译…

计算机网络:计算机网络概述:网络、互联网与因特网的区别

文章目录 网络、互联网与因特网的区别网络分类 互联网因特网基于 ISP 的多层次结构的互连网络因特网的标准化工作因特网管理机构因特网的组成 网络、互联网与因特网的区别 若干节点和链路互连形成网络&#xff0c;若干网络通过路由器互连形成互联网 互联网是全球范围内的网络…