【JavaScript】0.1 + 0.2 = 0.30000000000000004该怎样理解?

2024-09-07 23:08

本文主要是介绍【JavaScript】0.1 + 0.2 = 0.30000000000000004该怎样理解?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

如果你以前没了解过类似的坑,乍一看似乎觉得不可思议。但是某些语言下事实确实如此(比如 Javascript):

这里写图片描述

再看个例子,+1 后居然等于原数,没天理啊!

这里写图片描述

如果你不知道原因,跟着楼主一起来探究下精度丢失的过程吧。

事实上不仅仅是 Javascript,在很多语言中 0.1 + 0.2 都会得到 0.30000000000000004,为此还诞生了一个好玩的网站 0.30000000000000004。究其根本,这些语言中的数字都是以 IEEE 754 双精度 64 位浮点数 来存储的,它的表示格式为:

(s) * (m) * (2^e)

s 是符号位,表示正负。m 是尾数,有 52 bits。e 是指数,有 11 bits,e 的范围是 [-1074, 971](ECMAScript 5 规范),这样其实很容易推出 Javascript 能表示的最大数为:

1 * (Math.pow(2, 53) - 1) * Math.pow(2, 971) = 1.7976931348623157e+308

而这个数也就是 Number.MAX_VALUE 的值。

同理可推得 Number.MIN_VALUE 的值:

1 * 1 * Math.pow(2, -1074) = 5e-324

需要注意的是,Number.MIN_VALUE 表示的是最小的比零大的数,而不是最小的数,最小的数很显然是 -Number.MAX_VALUE。

可能你已经注意到,当计算 Number.MAX_VALUE 时,(Math.pow(2, 53) - 1) 的结果用二进制表示是 53 个 1,除了 m 表示的 52 个 bits 外,其实最前面的 1 bit 是隐藏位(隐藏位表示的永远是 1),设置隐藏位为的是能表示更大范围的数。(对于隐藏位我也不是很清楚,一说 “当 指数 e 的二进制位全为 0 时,隐藏位为 0,如果不全为 0,则隐藏位为 1,这应该是基于指数表达式的存储方式决定的,隐藏位也就是指数的底数里面的整数部分,尾数 m 则是指数中底数的 fraction 小数部分” 详见 Javascript 中小数和大整数的精度丢失问题)

复习了一些组成原理的知识后,我们再回到 0.1 + 0.2 这道题本身。我们都知道,计算机中的数字都是以二进制存储的,如果要计算 0.1 + 0.2 的结果,计算机会先把 0.1 和 0.2 分别转化成二进制,然后相加,最后再把相加得到的结果转为十进制。

我们先把 0.1 和 0.2 分别转化为二进制,十进制转为二进制这里就不多说了,整数部分 “除二取余,倒序排列”,小数部分 “乘二取整,顺序排列”。也可以用 Javascript 的 toString(2) 方法验证转换的结果。

// 0.1 转化为二进制
0.0 0011 0011 0011 0011...(0011循环)// 0.2 转化为二进制
0.0011 0011 0011 0011 0011...(0011循环)

当然计算机并不能表示无限小数,毕竟只有有限的资源,于是我们得把它们用 IEEE 754 双精度 64 位浮点数 来表示:

e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)

当然,真实的计算机存储中 m 并不会是一个小数,而是上面的小数点后的 52 bits,小数点前的 1 为隐藏位。

这里又出现一个问题,虽然我们已经明确 m 只能有 52 位(小数点后),但是如果第 53 位是 1,是该进位还是不进位?这里需要考虑 IEEE 754 Rounding modes,可以看下这篇文章 浮点数解惑,或者听我简单地解释下。

关于默认的舍入规则,简单的说,如果 1.101 要保留一位小数,可能的值是 1.1 和 1.2,那么先看 1.101 和 1.1 或者 1.2 哪个值更接近,毫无疑问是 1.1,于是答案是 1.1。那么如果要保留两位小数呢?很显然要么是 1.10 要么是 1.11,而且又一样近,这时就要看这两个数哪个是偶数(末位是偶数),保留偶数为答案。综上,如果第 52 bit 和 53 bit 都是 1,那么是要进位的。

另外,相加时如果指数不一致,需要对齐,一般情况下是向右移,因为最右边的即使溢出了,损失的精度远远小于左边溢出。

接下去就不难了:

e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
---------------------------------------------------------------------------e = -3; m = 0.1100110011001100110011001100110011001100110011001101 
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010
---------------------------------------------------------------------------e = -3; m = 10.0110011001100110011001100110011001100110011001100111
---------------------------------------------------------------------------e = -2; m = 1.0011001100110011001100110011001100110011001100110100(52位)
---------------------------------------------------------------------------
= 0.010011001100110011001100110011001100110011001100110100
= 0.30000000000000004(十进制)

9007199254740992 + 1 = 9007199254740992 的推理过程大同小异。

9007199254740992 其实就是 2 ^ 53。

 e = 0; m = 100000000000000000000000000000000000000000000000000000 (53个0)
+ e = 0; m = 1 
---------------------------------------------------------------------------e = 0; m = 100000000000000000000000000000000000000000000000000001

因为 m 只能有 52 位,而上面相加两数相加后 m 有 53 位(已经除去首位隐藏位),又因为 Rounding modes 的偶数原则,所以将 53 bit 的 1 舍去,所以大小跟 2 ^ 52 并没有变化,试想下,如果是 + 2,那么结果就不一样了。(ps:其实 2^53 在计算机存储中的 m 只能有 52 位,即只有 52 个 0)

事实上,当结果大于 Math.pow(2, 53) 时,会出现精度丢失,导致最终结果存在偏差,而当结果大于 Number.MAX_VALUE,直接返回 Infinity。

如果你觉得已经足够了解 IEEE 754 双精度 64 位浮点数 的运算性质了,不妨试试 玉伯 在 JavaScript 中小数和大整数的精度丢失 一文最后留下的思考题:

Number.MAX_VALUE + 1 == Number.MAX_VALUE;
Number.MAX_VALUE + 2 == Number.MAX_VALUE;
...
Number.MAX_VALUE + x == Number.MAX_VALUE;
Number.MAX_VALUE + x + 1 == Infinity;
...
Number.MAX_VALUE + Number.MAX_VALUE == Infinity;// 问题:
// 1. x 的值是什么?
// 2. Infinity - Number.MAX_VALUE == x + 1; 是 true 还是 false ?

之前类似如此的精度缺失问题,我都会推荐先将其乘以 10 的倍数,化为整数的方式:

(0.1 * 10 + 0.2 * 10) / 10 
=> 0.3

直到看到此文 你不一定知道的几个前端小知识:

2177.74*100
=> 217773.99999999997

楼主不禁又陷入了思考…

这篇关于【JavaScript】0.1 + 0.2 = 0.30000000000000004该怎样理解?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1146426

相关文章

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

Spring核心思想之浅谈IoC容器与依赖倒置(DI)

《Spring核心思想之浅谈IoC容器与依赖倒置(DI)》文章介绍了Spring的IoC和DI机制,以及MyBatis的动态代理,通过注解和反射,Spring能够自动管理对象的创建和依赖注入,而MyB... 目录一、控制反转 IoC二、依赖倒置 DI1. 详细概念2. Spring 中 DI 的实现原理三、

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.