为什么0.1+0.2=0.30000000000000004
JavaScript 作为一门诞生自上个世纪 90 年代的编程语言,从诞生之初就因为诡异的隐式类型转换等原因被黑,很多 JavaScript 的开发者还会吐槽浮点数加法的『奇葩』问题 — 为什么 0.1 + 0.2 在 JavaScript 中不等于 0.3,相信很多人都对这个问题的答案有一个大概的认识,但是都没有深入研究过,这个问题的答案让 William Kahan 在 1989 年获得图灵奖。
0.1 + 0.2
// 0.30000000000000004
其实有上述问题的不止 JavaScript 一门编程语言,几乎所有现代的编程语言都会遇到上述问题,包括 Java、Ruby、Python、Swift 和 Go 等等,你可以在 https://0.30000000000000004.com/ 中找到常见的编程语言在计算上述表达式的结果。这不是因为它们在计算时出现了错误,而是因为浮点数计算标准的要求。
我们习惯性的将编程中的浮点数和数学中的小数看做同一个东西,但这两个概念是有不同之处的:
- 编程中的浮点数的精度往往都是有限的,单精度的浮点数使用 32 位表示,而双精度的浮点数使用 64 位表示;
- 数学中的小数系统可以通过引入无限序列 ... 可以任意的实数;
在数学上我们总有办法通过额外的符号表示更复杂的数字,但是从工程的角度来看,表示无限精度的数字是不经济的,我们期望通过更小和更快的系统表示范围更大和精度更高的实数。浮点数系统是在工程上面做的权衡,IEEE 754 就是在 1985 年建立的浮点数计算标准,它定义了浮点数的算术格式、交换格式、舍入规则、操作和异常处理。讨论浮点数也无法脱离该标准,为了回答今天的问题,我们将从以下的两个角度触发:
- 二进制无法在有限地长度中精确地表示十进制中 0.1 和 0.2;
- 单精度、双精度浮点数的位数决定了它们能够表示的范围和精度上限;
单精度与双精度浮点数
在计算机中的小数有两种,定点和浮点。
定点: 小数点固定在 32/64 位中的某个位置,前面的是整数,后面的是小数。定点数的优点是很简单,大部分运算实现起来和整数一样或者略有变化,但是缺点则是表示范围太小,精度很差,不能充分运用存储单元。
浮点数: 就是设计来克服这个缺点的,它相当于一个定点数加上一个阶码,阶码表示将这个定点数的小数点移动若干位。由于可以用阶码移动小数点,因此称为浮点数。我们在写程序时,用到小数的地方,用浮点数表示,可以方便快速地对小数进行运算。
编程语言中的浮点数一般都是 32 位的单精度浮点数(float)和 64 位的双精度浮点数(double),部分语言会使用 float32 或者 float64 区分这两种不同精度的浮点数。想要使用有限的位数表示全部的实数是不可能的,不用说无限长度的小数和无理数,因为长度的限制,位数过长的有限小数在浮点数中都无法精确的表示。
在 IEEE-754 标准出现之前,业界并没有一个统一的浮点数标准,相反,很多计算机制造商都在设计自己的浮点数规则以及运算细节。
为了便于软件的移植,浮点数的表示格式应该有一个统一的标准。1985年,IEEE(Institute of Electrical and Electronics Engineers,美国电气和电子工程师协会)提出了 IEEE-754 标准,并以此作为浮点数表示格式的统一标准。几乎所有的计算机都支持该标准,从而大大改善了科学应用程序的可移植性 。
IEEE 754 标准中的单精度与双精度浮点数:
- 单精度浮点数(float): 占 4 个字节,用 32 位二进制描述。其中 1 位表示符号、8 位表示指数,最后 23 位表示有效数;
- 双精度浮点数(double): 占 8 个字节,用 64 位二进制描述。其中 1 位表示符号,11 位表示指数,最后 52 位表示有效数;

- 符号位(S): 第 1 位是正负数符号位(sign),0 代表正数,1 代表负数;
- 指数位(E): 中间的 8/11 位是指数(exponent),用来表示指数位数;
- 尾数位(M): 最后的 23/52 位是尾数(mantissa),表示浮点数有效数位数,超出的部分自动进一舍零。
浮点数在 Javascript 中的存储,与其他语言如 Java 和 Python 不同。所有数字(包括整数和小数)都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用64位精度来表示浮点数。
实际数字就可以用以下公式来计算:

S 是符号位,只有 0 和 1,分别表示正负;
M 是尾数位,有 52 位。IEEE 754 规定,注意,在计算机内部保存 M 时,尾数的最高位始终是1,因此我们完全可以省略掉该位。 比如:保存 1.01 的时候,只保存 01,等到读取的时候,再把第一位的 1 加上去。
E 是指数位,有 11 位,是一个无符号整数,取值个数是 2048(2048 = Math.pow(2,11)),范围是 [0,2047]。但是科学计数法中的指数是可以为负数的,所以 IEEE 754 规定,E 的真实值必须再减去一个中间数;对于11位的中间数是1023(对于 8 位的中间数是127)。 这样 [0,1022] 位表示的数值是 [ -1023, -1],[1023,2047]位表示的数值是 [0,1024]。
比如:4.5 转换成二进制就是 100.1,科学计数法表示是 1.001×2^2(2^2,前面的 2 表二进制,如同十进制 1001 = 1.001×10^2),舍去整数部分 1 后 M = 001。

它在内存中的二进制是:
0 10000000001 0010000000000000000000000000000000000000000000000000
浮点数的范围和精度
浮点数的范围
浮点数的范围是由指数位的位数来决定的。
浮点数范围就是浮点数能表达的最小值和最大值,只要找到了最大值,将它取反,变化为负数就是取小值了,而要想表示的值最大肯定是尾数最大并且指数最大。
单精度浮点数指数位由 8 位表示,取值个数是 2^8 = 256个,范围是 [0,255],去除 0(8 位全为 0000 0000,表示 0) 和 255(8 位全为 1111 1111,表示无穷大) 这两种特殊情况,还可取值 254 个,即 [1,254]。根据 IEEE 标准约定,减去中间数 127,指数的取值范围是 [-126,127],127 就是最大指数。
那么可以得到尾数为 11111111 11111111 11111111,指数为 11111110 时,单精度浮点数的数字最大值编码形式是:0 11111110 11111111111111111111111,公式:1.11111111 11111111 1111111 x 2 ^ 127 = 340282346638528859811704183484516925440,通常表示成 3.4028235 x E ^ 38。
单精度浮点数指数位 8 位,指数最大值 127,尾数 23 位,则最大数为 1.11111111111111111111111 × 2^127 = (2-2^-23) × 2^127 = 2^128 - 2^104。
// 单精度浮点数的范围
[-(Math.pow(2,128) - Math.pow(2,104)), Math.pow(2,128) - Math.pow(2,104)]
[-3.4028235×e^38, 3.4028235×e^38]
双精度浮点数指数位由 11 位表示,取值个数是 2^11=2048,范围是 [0,2047],去除 0 和 2047 这两种特殊情况,取值 [1,2046]。根据 IEEE 754 标准约定,减去中间数 1023,指数的取值范围是 [-1022,1023],1023 就是最大指数。同上,推导可得:
// 双精度浮点数的范围
[-1.7976931348623157×e^308, 1.7976931348623157×e^308]
浮点数的精度
浮点数的数据精度取决于尾数位。
浮点数的精度是指浮点数的有效数字的最大位数,从左边第一个不为 0 的数字开始的个数。
浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。
单精度浮点数的尾位数是 23 位,能表达的最大数字是 2^23 = 8388608,一共七位。这意味着最多能有 7 位有效数字,绝对保证可表达的是 6 位,因为大于该最大数字的数精度会丢失,即单精度浮点数的精度为 6~7 位有效数字。
双精度浮点数的尾位数是 52 位,能表达的最大数字是 2^52 = 4503599627370496,一共 16。同理,双精度浮点数的精度为 15~16 位。
浮点数的特殊值
零: 0 无法规范化,导致它不能以单精度或双精度值的规范化形式表示。 全零(指数和尾数部分全为0)的特殊位模式表示 0。 也可以通过设置符号位将 -0 表示为零,但 -0 和 0 的比较结果始终为相等。
# 单精度浮点数:0 0 00000000 00000000000000000000000 # 单精度浮点数:-0 1 00000000 00000000000000000000000非规约形式: 指数部分全为 0,尾数部分不全为0,那么这个浮点数将被称为非规约形式的浮点数。
一般是某个数字相当接近零时才会使用非规约型式来表示。 IEEE 754 标准规定: 非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为 0,对应的指数实际值也是 -126 而不是 -127。实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0。
无穷: 在 IEEE 754 浮点数中无穷的特征是指数部分全为1,尾数部分全为0,是一个比浮点数最大值大1的数。符号位为 0 表示正无穷数,为 1 表示负无穷数。
# 单精度浮点数:正无穷 0 11111111 00000000000000000000000 # 单精度浮点数:负无穷 1 11111111 00000000000000000000000NaN: NaN 翻译过来就是 Not a Number,它的特征是指数部分全为1,尾数部分不全为0。无论符号位是 0 还是 1,它始终都是代表着 NaN。
# 尾数部分不全为0 0 11111111 00000000000000000000001
二进制转十进制
口诀:整数二进制,从低位到高位(即从右往左)计算,用数值乘以 2 的位数(从 0 开始)幂次依次相加;小数二进制,高位到低位(即从左往右)计算,用数值乘以 2 的位数(从 1 开始)负幂次然后依次相加。
以 11001011.001 为例:
// 整数部分
1 * 2^0 = 1 * 1 = 1
1 * 2^1 = 1 * 2 = 2
0 * 2^2 = 0 * 4 = 0
1 * 2^3 = 1 * 8 = 8
0 * 2^4 = 0 * 16 = 0
0 * 2^5 = 0 * 32 = 0
1 * 2^6 = 1 * 64 = 64
1 * 2^7 = 1 * 128 = 128
整数部分相加:1 + 2 + 0 + 8 + 0 + 0 + 64 + 128 = 203
// 小数整分
0 * 2^-1 = 0 * 0.5 = 0
0 * 2^-2 = 0 * 0.25 = 0
1 * 2^-3 = 1 * 0.125 = 0.125
小数部分相加:0 + 0 + 0.125 = 0.125
二进制 11001011.001 转为十进制结果:203.125。
(203.125).toString(2)
// 11001011.001
十进制转二进制
我们日常生活中使用的数字基本都是 10 进制的,然而计算机使用二进制的 0 和 1 表示整数和小数,所有有限的十进制整数都可以无损的转换成有限长度的二进制数字,但是要在二进制的计算机中表示十进制的小数,就可能出现精度丢失问题。
二进制l转十进制规则:整数——除 2 取余,逆序排列;小数——乘 2 取整,顺序排列。
以 173.8125 为例:
# 整数部分,十进制转二进制。
# 除2取余,逆序排列
173 / 2 = 86 ... 1
86 / 2 = 43 ... 0
43 / 2 = 21 ... 1
21 / 2 = 10 ... 1
10 / 2 = 5 ... 0
5 / 2 = 2 ... 1
2 / 2 = 1 ... 0
1 / 2 = 0 ... 1
取余数逆序排列:10101101。
(173).toString(2)
// 10101101
# 小数部分,十进制转二进制
# 乘2取整,顺序排列
0.8125 * 2 = 1.625
0.625 * 2 = 1.25
0.25 * 2 = 0.5
0.5 * 2 = 1
取整数顺序排列:1101。
(0.8125).toString(2)
// 0.1101
二进制小数精度丢失
根据转换规则,将十进制小数 0.1 转为二进制:
0.1 * 2 = 0.2
0.2 * 2 = 0.4 # 注意这里
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4 # 注意这里,循环开始
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2
...
结果发现有限十进制小数 0.1 却转化成了无限二进制小数 0.00011001100...。
(0.1).toString(2)
# 0.0001100110011001100110011001100110011001100110011001101
64 位浮点数,尾数位是 52 位,也就是说,所有小数转换成二进制的位数超过 52 的会被截掉,从而导制精度丢失。
十进制小数能被转化为有限二进制小数的的最后一位必然以 5 结尾(因为只有 0.5 * 2 才能变为整数)。所以十进制中,一位小数 0.1 ~ 0.9 当中除了 0.5 之外的值在转化成二进制的过程中都丢失了精度,二位小数中的 0.25,三位小数中的 0.125 。。。,也就是:必需有一个整数 n 使 M × 2^n = 0.5 成立。
再回到 0.1 + 0.2 的问题,它们的二进制运算过程如下:
# 二进制加法的法则:把两个数小数点对齐,从最右边开始计算
# 0+0=0
# 0+1=1+0=1
# 1+1=0 (进位为1)
# 1+1+1=1 (进位为1)
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
# 计算的二进制结果转成十进制正好是 0.30000000000000004
为什么有时精度不丢失呢?
为什么 x = 0.1 能得到 0.1?
根据上面可知,双精度浮点数的精度为 15~16 位。JavaScript 是采用 64 位 浮点数表示小数,最大精度是 16 位。也就是说,会将小数点后的第 17 位进行凑整处理。
0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1
// 所以,你看到的 0.1 实际上并不是 0.1。
// 不信你可用更高的精度试试:
0.1.toPrecision(21) = 0.100000000000000005551
那么,在将双精度浮点数转换为十进制的数字字符串时,为什么有些小数后的位数会有小于 17 位的呢?
0.1 + 0.2 = 0.30000000000000004 // 17 位有效数字
0.1 + 0.7 = 0.7999999999999999 // 16 位有效数字
0.1 + 0.3 = 0.4 // 1位有效数字
上面两个运算,输出结果均是真实算数结果的近似值,然而却有不同的有效数字位;在浏览器控制台里输入0.1 (我们知道,这个不是精确的值),控制台显示的是 0.1,而不是看起来更精确的 0.10000000000000001。
很明显,在控制台输出的浮点数近似值都是经过一定的规则来截断的。同时这种表现不仅限于 JS,所有采用 IEEE 754 双精度浮点数标准的语言,如:Java、Node、Python,都有同样的表现。
那么,IEEE 754 标准是按照什么规则来实现双精度浮点数的截断的?
IEEE 754 规定,浮点数(二进制)被转成十进制数字字符串,当这个字符串转回浮点数(二进制)时,必须要跟原来的数(二进制)相同。对双精度浮点数来说,十进制字符串使用 17 位有效数字即可保存原始二进制值。
还是以 0.1 为例,它在内存中的二进制表示转换为十进制的数字字符串为:
(0.1).toPrecision(60)
// 0.100000000000000005551115123125782702118158340454101562500000
当我们将它进行 17 位凑整处理后,有效数为 0.10000000000000001,控制台显示的显示 0.1。事实上,浮点数的小数中有许多不同的十进制数对应的二进制数是相同的。以上三个值:0.1、0.10000000000000001、0.100000000000000005551115123125782702118158340454101562500000 在内存中的 64 位浮点数的二进制数是完全相同的。
# 对双精度浮点数来说,十进制字符串使用 17 位有效数字即可保存原始二进制值
# 这三个数,在内存中是一样的
# 系统选择最短的来展示
(0.1).toString(2)
(0.10000000000000001).toString(2)
(0.1000000000000000055511151231257827021181583404541015625).toString(2)
// 0.0001100110011001100110011001100110011001100110011001101
简单来说:在所有能表示同一二进制数的十进制字符中,系统一般会选择位数最少的来展示。
怎么让 0.1 + 0.2 = 0.3?
将浮点数转为整数运算,再对结果做除法
把需要计算的数字扩大(乘以 10 的 n 次幂)成整数,计算完成后再进行降级(除以 10 的 n 次幂),这是大部分语言处理精度问题常用方法。
(0.1 * 10 + 0.2 * 10) / 10
// 0.3
注意:有些小数可能还是存在精度问题。因为,小数扩大为整数时,精度也有可能丢失:
35.41 * 100
// 3540.9999999999995
对结果进行指定精度的四舍五入
用 toFixed() 转换为 String 类型,再强制 Number() 转换为数字。
Number((0.1+0.2).toFixed(2))
// 0.3
注意: toFixed() 用来做四舍五入,会有一些 Bug。
1.335.toFixed(2)
// 1.33
// 因为 1.335 本身是一个精度丢失的值,它实际是 1.3349999999999999645...
你可以重新封装一个该方法:最后一位开始,判断值是否大于等于5来决定需不需要进位,如果需要进位先把小数乘以倍数变为整数,加1之后,再除以倍数变为小数,这样就不用一位一位的进行判断。
采用第三方库
把浮点数转化为字符串,模拟实际运算的过程。如:decimal.js、bignumber.js、number-precision
为什么 JavaScript 最大安全整数是2^53-1?
根据 ECMAScript 标准,JavaScript 中只有 Number 一种数字类型,是基于 IEEE 754 标准的双精度 64 位二进制格式的值,其安全整数范围在(-2^53, 2^53)之间(不含两个端点)。
64 位双精度浮点型存储格式:

即,1 位符号位,11 位指数位,52 位尾数位。
尾数位是 52 位,那么最大可以精确表示的数应该是 2^52 - 1,为什么会是 2^53-1呢?
因为,IEEE754 规定尾数第一位隐含为 1,可以省略,表达公式即:

所以,实际的有效数字是 53 位。
那么,现在唯一的问题是最高位始终为 1,我们无法随意使用所有位。这一问题解决如下:
比如,需要一个指数为 52,尾数位为 0 + 52位1 的数字,则可用尾数位 52位1,指数为 51 表示:
2^52 × 0.11111111 11111111 11111111 11111111 11111111 11111111 1111
2^51 × 1.11111111 11111111 11111111 11111111 11111111 11111111 111
类似十进制:0.31415 × 10^5 = 3.1415 × 10^4。
因此,Javascript 能表达的最大整数是尾数位为 53 位 1 的数字,换算成十进制就是 2^53-1:
(1 * 2^52) + (1 * 2^51) + ... + (1 * 2^0) = 1 * 2^53 - 1
而所谓安全整数,是指数字整数与其在 JavaScript 中的表示之间存在一对一的映射; 不安全整数是指两个或多个 JavaScript 整数可以同一数学整数:
Math.pow(2, 53) // 9007199254740992
Math.pow(2, 53) + 1 // 9007199254740992
/** 尾数位只有52位(不包括首位默认) **/
2^53 = (-1)^0 × 2^53 × 1.(52个0)(0) // 尾数位最后一位(0)会被截掉。
2^53+1 = (-1)^0 × 2^53 × 1.(52个0)(1) // 同样,尾数位最后一位(1)会被截掉。
2^53 和 2^53+1 最后的表达公式都是:(-1)^0 × 2^53 × 1.(52个0),不同的是,2^53 丢失了 (0),不影响要表达的数字,而 2^53+1 丢失了 (1),其表达的数字已不准确。
类似十进制:1.00 × 10^2 表达数字 100,截掉最后一个 0,1.0 × 10^2 表达的仍是 100;而 1.01 × 10^2 表达的是 101,截掉最后一个 1,其表达值就不对了。
由于数字 9007199254740992 即可以是 2^53,也可以是 2^53+1,所以 2^53 也是不安全整数。
注意: 从上面讲到的浮点数范围可知,双精度浮点是取值范围是很大的(1.7976931348623157×e^308),但是超过 2^53-1 后,很多数字是不精确的。其根本原因在于,指数的最大取值是 1023,而有效数只有 53 位。
注意: 大于 2^53-1 的整数中,只有 尾数 × 2^指数 表示的数字是准确的,指数最大取值 1023。
Javascript 中关于 Number 取值范围的几个属性:
- MAX_VALUE: 最大数值;
MIN_VALUE: 最小正数值,即最接近 0 的正数 (实际上不会变成 0);
MAX_SAFE_INTEGER: 最大的安全整数;
- MIN_SAFE_INTEGER: 最小的安全整数。
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324
Number.MAX_SAFE_INTEGER // 9007199254740991
Math.pow(2, 53) - 1 // 9007199254740991
Number.MIN_SAFE_INTEGER // -9007199254740991
-(Math.pow(2, 53) - 1) // -9007199254740991
参考资料
JavaScript 关于 IEEE 754 双精度浮点数的实现