0.1+0.2 !== 0.3?

栏目: JavaScript · 发布时间: 6年前

内容简介:众所周知,JavaScript在计算某些浮点数的运算时会出现精度的丢失,比如你在控制台输入我们知道,计算机里所有的数据最终都是以二进制保存的,当然数字也一样。所以当计算机计算不同的语言有不同的存储标准,这里我们暂且只讨论JavaScript的存储标准。JavaScript中所用的数字包括整数和小数,都只有一种类型就是

众所周知,JavaScript在计算某些浮点数的运算时会出现精度的丢失,比如你在控制台输入 0.1+0.2 ,得到的结果是 0.30000000000000004 而不是 0.3 ,原因是什么?

世界上有两种人,懂二进制和不懂二进制的人

我们知道,计算机里所有的数据最终都是以二进制保存的,当然数字也一样。所以当计算机计算 0.1+0.2 的时候,实际上计算的是这两个数字在计算机里所存储的二进制,那么 0.1 在JavaScript里存储的二进制到底是多少? 我们先根据十进制转二进制的方法,把 0.1 转化为二进制是: 0.0001100110011001100... (1100循环),然后把 0.2 转化为二进制是: 0.00110011001100... (1100循环)。 我们发现,它们都是无限循环的二进制。显然,计算机当然不会用自己“无限的空间”去存储这些无限循环的二进制数字。那对于这类数据该怎么办?

JavaScript如何存储无限循环的二进制小数?

不同的语言有不同的存储标准,这里我们暂且只讨论JavaScript的存储标准。JavaScript中所用的数字包括整数和小数,都只有一种类型就是 Number ,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数(相关的还有float 32位单精度),具体的双精度浮点数的存储方式这里不再赘述(可以看后面章节的详细描述),我们只需要知道,在二进制科学表示法中,双精度浮点的小数部分最多只能保留52位(比如 1.xxx...*2^n ,这里 x 最多保留52位),再回过头看 0.1 的二进制表示:

0.00011001100110011001100110011001100110011001100110011001 10011...
复制代码

空格前为第53个有效数字,如何舍去后面的数字,这里遵从“0舍1入”,那么舍去之后实际上就是:

0.00011001100110011001100110011001100110011001100110011010
复制代码

同理我们得到 0.2 的舍去之后实际存储的二进制为:

0.0011001100110011001100110011001100110011001100110011010
复制代码

二者相加:

0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
复制代码

我们把结果根据公式或者 工具 转为十进制:

0.1+0.2 !== 0.3?

可以看到结果正好为: 0.30000000000000004

注:大多数语言中的小数默认都是遵循 IEEE 754 的 float 浮点数,包括 JavaRuby 、Python,本文中的浮点数问题同样存在。

浮点数是如何保存的

0.1+0.2 !== 0.3?

在计算机中,浮点表示法,分为三大部分:

  • 第一部分用来存储符号位(sign),用来区分正负数,0表示正数
  • 第二部分用来存储指数(exponent)
  • 第三部分用来存储小数(fraction)

双精度浮点数一共占据64位:

  • 符号位(sign)占用1位
  • 指数位(exponent)占用11位
  • 小数位(fraction)占用52位

这里的符号位、指数位、小数位跟二进制是如何联系在一起呢? 我们以 78.735 为例

0.1+0.2 !== 0.3?
最后的的 1.001110011*2^6 我们称为二进制的科学记数法,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这就叫 浮点数 。 我们对号入座,先把指数部分 6 转化为二进制是 110

,最终为:

0(sign) 00000000110(exponent) 00111001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
复制代码

(这是错误的,具体为什么错,继续看下文)

我们再根据双精度规范,来看看上文提到的 0.1 到底是如何存储的,我们已知它的二进制是:

0.00011001100110011001100110011001100110011001100110011001 10011...
复制代码

转化为科学表示法就是:

1.1001100110011001100110011001100110011001100110011001*2^-2
复制代码

也就是说 0.1 的:

0
1001100110011001100110011001100110011001100110011001
-2

到这里我就懵逼了, -2 怎么转为二进制呢,虽然双精度浮点规范规定了一个符号位,但是这个符号位表示的是整个数据的正负,而非指数的正负,难道还要保留一位专门存储指数的正负吗?答案是否定的,为了减少不必要的麻烦,IEEE规定了一个偏移量,这个偏移量是干嘛用的呢,就是对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也变为正数啦。为了使所有的负指数加上这个偏移量都能够变为正数,这个偏移量的设置也是有规律的。 以double双精度为例,我们知道它的指数部分是二进制的11位,那么能够表示的数据范围就是 0~2047 ,IEEE规定一个大概在中间数的位置 1023 为双精度的偏移量。

  1. 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias 。 此时e最小值是1,则 1-1023= -1022 ,e最大值是 2046 ,则 2046-1023=1023 ,可以看到,这种情况下取值范围是 -1022~1013
  2. 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为 1-Bias ,即 1-1023= -1022
  3. 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷, NaN(not a number) 。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。

这个时候我们再看 78.735 的指数部分如何存储,需要 6+1023 就是 1029 ,转化为二进制就是: 10000000101 ,所以 78.735 正确存储方式为:

0 10000000101 00111001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
复制代码

同理,你是否也知道 0.1 的双精度的浮点存储形式了呢?

浮点数值的范围

如果你认真读到了这里,想必你应该能推算出JavaScript的所能表示的数值范围了吧。 e的最大值是1023。 1.111..(52位)..11*2^1023 转为普通二进制就是:

1 111..(52位)..11 000..(1023-52就是971位)..00
复制代码

把二进制转为十进制就是:

0.1+0.2 !== 0.3?
我们会发现这个值和 Number.MAX_VALUE 的值一致,都是 1.7976931348623157e+308 。 但实际上这个值还不算最大,比如我们在此数值基础上继续加一些数,发现并没有返回 Infinity

0.1+0.2 !== 0.3?
所以 Number.MAX_VALUEInfinity 之间还存在一些数,根据IEEE规范我们可以得知,正无穷当且是指数部分全为1(指数部分的最大值 Math.pow(2,11)-1-1023 == 1024

),小数部分为0的时候,就是:

1.000...*2^1024
复制代码

所以 Math.pow(2,1024) 就是正无穷,那么其实JavaScript所能存储的最大数字是 Math.pow(2,1024)-1 。 但是 Number.MAX_VALUEMath.pow(2,1024) 之间的数据我们无法正常表示出来,精度会丢失。 同理也可推算最小数。

JavaScript的安全最大整数

所谓安全范围,就是我们在这个范围内计算不会出现精度的丢失。 根据双精度的定义,可以得知,最大的安全整数:

1.11..(52位)*2^52
复制代码

转为十进制就是 Math.pow(2,53)-1 ,即 9007199254740991

在JavaScript中,有 Number.MAX_SAFE_INTEGER 来表示最大安全整数

0.1+0.2 !== 0.3?
我们发现和我们自己推算出来的值是一样的。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Learning PHP, MySQL, and JavaScript

Learning PHP, MySQL, and JavaScript

Robin Nixon / O'Reilly Media / 2009-7-21 / USD 39.99

Learn how to create responsive, data-driven websites with PHP, MySQL, and JavaScript - whether or not you know how to program. This simple, streamlined guide explains how the powerful combination of P......一起来看看 《Learning PHP, MySQL, and JavaScript》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具