为什么 JavaScript 中的 0.1 + 0.2 不等于 0.3

本文作者为 360 奇舞团前端开发工程师

在使用 JavaScript 处理运算时,有时会碰到数字运算结果不符合预期的情况,比如经典的 0.1 + 0.2 不等于 0.3。当然这种问题不只存在于 JavaScript,不过编程语言的一些原理大致相通,我以 JavaScript 为例解释这种问题,并说明前端如何尽可能保证数字精确。

let a = 0.1,b=0.2,c=0.3
console.log(a + b === c) //false

1. 计算机数字如何存储

理解类似问题的基础,首先要理解计算机数字的处理方式。计算机的一切信息都是二进制,数字也不例外,所有数字都是一段二进制。

在 JavaScript 中存储数字的二进制有 64 位,即我们常说的 64 位双精度浮点型数字。每个数字对应的 64 位二进制分为三段:符号位、指数位、尾数位。

其中符号位在六十四位的第一位,0 表示正数,1 表示负数。符号位之后的 11 位是指数位,决定了数字的范围。指数位之后的 52 位是尾数位,决定了数字的精度。

在 JavaScript 中,双精度浮点型的数转化成二进制的数保存,读取时根据指数位和尾数位的值转化成双精度浮点数。

比如说存储 8.8125 这个数,它的整数部分的二进制是 1000,小数部分的二进制是 1101。这两部分连起来是 1000.1101,但是存储到内存中小数点会消失,因为计算机只能存储 0 和 1。

1000.1101 这个二进制数用科学计数法表示是 1.0001101 * 2^3,这里的 3 (二进制是 0011)即为指数。

可使用如下代码查看:

function getBinaryRepresentation(number) {const buffer = new ArrayBuffer(8); // 创建一个包含8字节的   ArrayBufferconst view = new DataView(buffer); // 创建一个DataView以便访问内存中的数据view.setFloat64(0, number); // 将浮点数写入到内存中// 读取内存中的字节,并将其转换为二进制字符串const binaryString = Array.from(new Uint8Array(buffer)).map(byte => byte.toString(2).padStart(8, '0')).join('');// 将二进制字符串分割为符号位、指数位和尾数位const signBit = binaryString[0];const exponentBits = binaryString.substring(1, 12);const mantissaBits = binaryString.substring(12,64);return { signBit, exponentBits, mantissaBits };
}const number = 8.8125; // 要展示的数字
const { signBit, exponentBits, mantissaBits } = 	getBinaryRepresentation(number);console.log(`符号位: ${signBit}`);
console.log(`指数位: ${exponentBits}`);
console.log(`尾数位: ${mantissaBits}`);符号位: 0
指数位: 10000000010
尾数位: 0001101000000000000000000000000000000000000000000000

现在我们很容易判断符号位是 0,尾数位就是科学计数法的小数部分 0001101。指数位用来存储科学计数法的指数,此处为 3。指数位有正负,11 位指数位表示的指数范围是 -1023~1024,所以指数 3 的指数位存储为 1026(3 + 1023)。

可以判断 JavaScript 数值的最大值为 53 位二进制的最大值:2^53 -1。

PS:科学计数法中小数点前的 1 可以省略,因为这一位永远是 1。比如 0.5 二进制科学计数为 1.00 * 2^-1。

2. 为什么会产生小数精度问题

首先补充一下小数的二进制的计算方法:

十进制小数转为二进制与整数相反,需要每次乘以 2
8.8125
o.8125*2 = 1.625  => 1
0.625*2 = 1.25      =>1
0.25*2 = 0.5          =>0
0.5*2 = 1                 =>1
小数部分为 1101
二进制小数转为十进制
1*2^-1 + 1*2^-2 + 0*2^-3 + 1*2^-4

在了解数字的存储后,很容易理解小数精度问题,因为十进制有 Π 这种无限循环数字,二进制也有循环数字。比如让 0.1 变为二进制,按照二进制转换永远会有余数,所以会是一个无限循环的二进制 0.0001 1001 1001 1001...(1100循环)。0.2 也是同理  0.0011 0011 0011 0011...(0011循环)。

所以当两个浮点数相加时,结果会有一些误差。比如 0.1 + 0.2 ,实际上是 0.0001 1001 1001...(1001循环) + 0.0011 0011 0011...(0011循环),如果截取于第 52 位,就会得到一个有误差的结果,转为十进制为0.30000000000000004,与 0.3 不相等。

3. 前端如何保证小数准确

首先出于安全性及准确性考虑,重要的数字计算应该交给服务端负责,相对于前端,服务端有更成熟稳定的数字处理方法,安全性也会更高。

当然前端有时也需要一些精确的数字计算,比如一些动画处理、定时器处理以及一些条件判断等。我简单列举几种方法供大家参考:

  • toFixed 指定小数位数 这种方法比较简单,不过有个点要注意,这个方法是四舍五入,但有时候看上去并不会,比如 2.55.toFixed(1) 显示的结果是 2.5 而不是 2.6。这是因为 2.55 二进制存储的值并不精确,调用 2.55.toPrecision(100) 可以看到这个数的实际值是 2.5499..... ,所以截取一位四舍五入是 2.5。再举一个例子 (2.449999999999999999).toFixed(1) = 2.5,因为这个数与 2.45 的差值小于 Number.EPSILON。

  • 将小数转为整数计算 这个方法的问题是转换会增加额外的复杂度和计算量,在某些场景下,可能会导致数值溢出问题。

  • 第三方库 精确计算推荐使用成熟的库,像 BigNumber.js、decimal.js ,进行高精度的浮点数计算。原理是把数字计算变为字符串计算。

JavaScript 的计算比较复杂,由于没有细分数字类型,底层计算以二进制进行,存储值、计算值都有可能因为精度丢失而不准确,而显示值可能会因为浏览器等宿主环境不同而有差别,所以一定要注意经常产生精度丢失的地方。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

52bfb57d5d4a31da80da3e02409868f2.png

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

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

相关文章

Matter 笔记1-环境准备,编译

不要远程登录Ubuntu输入以下命令,原因:ubuntu/linux上的http代理设置 1. 准备 1.1 工具 Ubuntu 22.04 LTSClash 里General的端口设置到ubuntu 的网络设置里 1.2 代码 这里使用芯科整理过的代码 git clone https://github.com/SiliconLabs/matter.…

Linux:kubernetes(k8s)探针ReadinessProbe的使用(9)

本章yaml文件是根据之前文章迭代修改过来的 先将之前的pod删除,然后使用下面这个yaml进行生成pod apiVersion: v1 # api文档版本 kind: Pod # 资源对象类型 metadata: # pod相关的元数据,用于描述pod的数据name: nginx-po # pod名称labels: # pod的标…

Requests教程-15-文件上传与下载

领取资料,咨询答疑,请➕wei: June__Go 上一小节,我们学习了requests的HTTPS请求方法,本小节我们讲解一下在requests文件上传与下载。 文件上传 使用requests库上传文件时,需要使用files参数,并将文件打…

仪酷LabVIEW OD实战(4)——Object Detection+OpenVINO工具包快速实现yolo目标检测

‍‍🏡博客主页: virobotics(仪酷智能):LabVIEW深度学习、人工智能博主 🎄所属专栏:『仪酷LabVIEW目标检测工具包实战』 📑上期文章:『仪酷LabVIEW OD实战(3)——Object Detectiononnx工具包快速…

Python(38):Request的data需入参是json,用转换json.dumps(data)

Python接口自动化测试遇到问题:误传str类型给request 接口请求数据用str传参报错,请求响应报错 排查原因:查看服务器报错是Json解析报错。 1.1、如果直接入参,进行request请求的数据: data请求值为: reqData {&quo…

【Python】6. 基础语法(4) -- 列表+元组+字典篇

列表和元组 列表是什么, 元组是什么 编程中, 经常需要使用变量, 来保存/表示数据. 如果代码中需要表示的数据个数比较少, 我们直接创建多个变量即可. num1 10 num2 20 num3 30 ......但是有的时候, 代码中需要表示的数据特别多, 甚至也不知道要表示多少个数据. 这个时候,…

vue面试--9, 1 ObjectProperty与vue3Proxy区别。2 MVVM的理解 3 双向绑定原理?

1 ObjectProperty与vue3Proxy区别 2 MVVM的理解 3 双向绑定原理?

NIO核心二:通道Channel

一、简单介绍 通道(Channel)是java.nio的第二个创建概念。Channel用于在缓冲区和位于通道另一侧的实体(通常是一个文件或者是一个套接字)之间有效的传输数据。只不过Channel本身不能直接访问数据,Channel只能和Buffer进行交互。 1.NIO的通道和流的区别 通道可以同…

Web自动化测试—webdriver的环境配置

🔥 交流讨论:欢迎加入我们一起学习! 🔥 资源分享:耗时200小时精选的「软件测试」资料包 🔥 教程推荐:火遍全网的《软件测试》教程 📢欢迎点赞 👍 收藏 ⭐留言 &#x1…

猫毛过敏又不想扔掉猫怎么办?如何养猫?热门宠物空气净化器分享

养了猫咪一年多,忽然发现自己患上了过敏性鼻炎和结膜炎,就是那种一靠近猫咪就会不断打喷嚏、流鼻涕、流眼泪的症状。有时候还会感到眼睛发痒,发红。有没有什么好的方法治疗过敏性鼻炎呢? 医生建议,从根本上解决问题需…

数据结构(八)——初识单链表

😀前言 单链表是数据结构中最基本的一种链表结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。单链表具有灵活性和动态性,可以根据需要插入、删除和查找元素,适用于各种场景和问题的解决。 在本篇文章…

Java 属性可见性和TypeScripta 属性可见性区别

Java 中默认(无修饰符)的可见性对应的是包级私有(package-private),这是 Java 特有的可见性修饰符,有时也称为默认可见性,包级私有的可见性意味着只有同一个包中的其他类才能访问该成员&#xf…