在 js 中进行数学的运算时,会出现 0.1+0.2=0.300000000000000004 的结果,一开始认为是浮点数的二进制存储导致的精度问题,但这似乎不能很好的解释为什么在同样的存储方式下 0.3+0.4=0.7 可以得到正确的结果。本文主要通过浮点数的二进制存储及运算,和 IEEE754 下的舍入规则,解释为何会出现这种情况。
浮点数的二进制存储
JavaScript 遵循 IEEE754 标准,在 64 位中存储一个数据的有效数字形式。

- 第 0 位为符号位,0 表示正数 1 表示负数;
- 第 1 到 11 位存储指数部分;
- 第 12 到 63 位存小数部分(尾数部分)(即有效数字)。
由于二进制的有效数字总是表示为 1.xxx…的形式,尾数部分在规约形式下的第一位默认为 1,故存储时第一位省略不写,尾数部分 f 存储有效数字小数点后的 xxx…,最长 52 位。因此,JavaScript 提供的有效数字最长为 53 个二进制位(尾数部分 52 位+被省略的 1 位)。 以 0.1、0.2、0.3、0.4 和 0.7 的二进制形式为例:
0.1->0.0001100110011...(0011无限循环)->0-01111111011-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.2->0.001100110011...(0011无限循环)->0-01111111100-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.3->0.01001100110011...(0011无限循环)->0-01111111101-(1 .)0011001100110011001100110011001100110011001100110011(舍)
0.4->0.01100110011...(0011无限循环)->0-01111111101-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.7->0.101100110011...(0011无限循环)->0-01111111110-(1 .)0110011001100110011001100110011001100110011001100110(舍)
对于 52 位之后进行舍入运算,此时可看作 0 舍 1 入(具体舍入规则在第三部分详细说明),有精度损失。
对阶运算
由于指数位数不同,运算时需要进行对阶运算。对阶过程略,0.1+0.2 与 0.3+0.4 的尾数求和结果分别如下:
0.1+0.2->10.0110011001100110011001100110011001100110011001100111
0.3+0.4->10.1100110011001100110011001100110011001100110011001101
求和结果需规格化(有效数字表示),右规导致低位丢失,此时需对丢失的低位进行舍入操作:
0.1+0.2->1.00110011001100110011001100110011001100110011001100111->1.0011001100110011001100110011001100110011001100110100(入)
0.3+0.4->1.01100110011001100110011001100110011001100110011001101->1.0110011001100110011001100110011001100110011001100110(舍)
即:
00111->0100
01101->0110
此处同样有精度损失。在这里我们可以发现,0.3+0.4 对阶阶运算且规格化后的运算结果与 0.7 在二进制中的存储尾数相同(可对照尾数后几位),而 0.1+0.2 的运算结果与 0.3 的存储尾数不同,且 0.1+0.2 转化为十进制时结果为 0.300000000000000004。
此时,虽然 0.1+0.2 与 0.3+0.4 进行舍入操作的近似位都为 1,但一入一舍导致计算结果与“标准答案”的异同。
IEEE754 标准下的舍入规则
维基百科对最近偶数舍入原则的解释如下:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式),即会将结果舍入为最接近(精度损失最小)且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以 0 结尾的)。
首先要注意的是,保留小数不是只看后面一位或者两位,而是看保留位后面的所有位。

如图,可以看到近似需要看三位,保留位(近似后的最低位)、近似位(保留位的后一位)、粘滞位(sticky bit 近似位后的所有位进行或运算后看作一位)。
当粘滞位为 1 时,舍入规则可以看作 0 舍 1 入,近似位为 0 舍,近似位为 1 入(即第一部分小数二进制存储为 52 位尾数时所进行的舍入操作)。
当粘滞位为 0 时,若近似位为 0 则舍去。
当粘滞位为 0 时,若近似位为 1,无论舍入精度损失都相同,故需取舍入两种结果中的偶数:保留位为 1 时入,保留位为 0 时舍(即第二部分对阶运算规格化时的舍入操作)。
0.1 + 0.2 的计算过程
十进制转成二进制
在 JS 内部所有的计算都是以二进制方式计算的。 所以运算 0.1 + 0.2 时要先把 0.1 和 0.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.8*2=1.6======取出整数部分1
0.6*2=1.2======取出整数部分1
所以0.1转化成二进制是:0.0001 1001 1001 1001......
0.2转化成二进制的算法:
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.8*2=1.6======取出整数部分1
0.6*2=1.2======取出整数部分1
所以0.2转化成二进制是:0.0011 0011 0011 0011......
这里要注意 0.1 和 0.2 转成的二进制是无穷的。另外在现代浏览器中是用浮点数形式的二进制来存储二进制,所以还要把上面所转化的二进制转成浮点数形式的二进制。
转成浮点数
浮点数分为单精度对应 32 位操作系统和双精度对应 64 位操作系统。目前的操作系统大多是 64 位操作系统,故这里只解释一下二进制如何转成双精度浮点数的二进制。
双精度浮点数用 1 位表示符号位,11 位表示指数位,52 位表示小数位,如下图所示:

- 符号位:正数为 0,负数为 1;
- 指数位:阶数+偏移量,阶数是:

e 为阶码的位数。偏移量是把小数点移动到整数位只有 1 时移动的位数,正数表示向左移,负数表示向右移;
- 小数位:即二进制小数点后面的数。
接下来把 0.1 转成的二进制 0.0001100110011001……转成浮点数形式的二进制。
先要把小数点移动到整数位只有 1,要向右移动 4 位,故偏移量为 −4,通过指位数的计算公式

把 1019 转成二进制为 1111111011,不够 11 位要补零,最终得出指位数为 01111111011;
小数位为 100110011001…… ,因为小数位只能保留 52 位,第 53 位为 1 故进 1。
转换结果如下图所示:

同理,再把 0.2 转成的二进制 0.0011 0011 0011 0011…… 转成浮点数形式的二进制,转换结果如下图所示:

浮点数相加
浮点数相加时,需要先比较指位数是否一致,如果一致则小数位直接相加,如果不一致,要先把指位数调成一致的,指位数小的向大的调整。
为了行文方便,把 0.1 转成的浮点数称为为 0.1,把 0.2 转成的浮点数称为 0.2。
0.1 的指数位是 1019 ,0.2 的指数位是 1020 。故要把 0.1 的指数位加 1,即把 0.1 的小数点向左移动 1 位,另外浮点数的整数位固定为 1,过程如下所示
1.1001100110011001100110011001100110011001100110011010 原先
0.11001100110011001100110011001100110011001100110011010 移动后
0.1100110011001100110011001100110011001100110011001101 将小数的第53位舍去,因为为0故不需进1
导致 0.1 的小数位变成如下所示:

现在 0.1 和 0.2 的指数位相同了,把小数位直接相加。
1100110011001100110011001100110011001100110011001101 0.1的小数位
+ 1001100110011001100110011001100110011001100110011010 0.2的小数位
= 10110011001100110011001100110011001100110011001100111
// 会发现现在的小数位多出了一位,超出了52位,故要把小数位最后一位截掉,小数位最后一位是1,故要进1,如下所示:
10110011001100110011001100110011001100110011001100111
1011001100110011001100110011001100110011001100110100
截掉小数位的最后一位相当把小数点向左移了一位,故指数位要加 1,此时的指数是 0.2 的指数 1021 ,加 1 后变成 1021 ,转成二进制为 01111111101 ,那么相加后的浮点数如下所示:

浮点数转成十进制
二进制浮点数计算结束后,把结果(二进制的浮点数)转成十进制,其转换公式为

s 是符号位为 0 或 1,e 为浮点数指数位转成十进制的值,i 表示小数位从左到右的位数,第一位 i=1 ,

表示每一位的值为 0 或 1。
那么按着公式把二进制的浮点数转成十进制:

结果如下所示:
0.3000000000000000444089209850062616169452667236328125
由于精度问题,只取到 0.30000000000000004。
答案
0.1+0.2 不等于 0.3 ,因为在 0.1+0.2 的计算过程中发生了两次精度丢失。第一次是在 0.1 和 0.2 转成双精度二进制浮点数时,由于二进制浮点数的小数位只能存储 52 位,导致小数点后第 53 位的数要进行为 1 则进 1 为 0 则舍去的操作,从而造成一次精度丢失。第二次在 0.1 和 0.2 转成二进制浮点数后,二进制浮点数相加的过程中,小数位相加导致小数位多出了一位,又要让第 53 位的数进行为 1 则进 1 为 0 则舍去的操作,又造成一次精度丢失。最终导致 0.1+0.2 不等于 0.3 。
拓展
若你回答出来,面试官还可能继续问你:“ 0.1+0.2 不等于 0.3 会引起那些 BUG?”
可以这样回答:“ 会引起统计页面展示错乱的 BUG,还有 300.01 优惠 300 元后,支付金额不足 0.01 元等类似的 BUG。”
还可能继续问道:“怎么解决 0.1+0.2 不等于 0.3 这个问题”。
可以这样回答:“可以用 Math.js 数学计算库来解决,或者用 toFixed()给计算结果四舍五入,但是 toFixed()在 chrome 或者火狐浏览器下四舍五入也有精度误差。可以用 Math.round 来解决精度误差,比如要把 2.55 四舍五入保留 1 位小数,先把 2.55∗10 得到 25.5 ,再用 Math.round 取整 25.5 ,会得到 25,再把 25÷10 得到 2.5 ,就这样间接实现了四舍五入。可以用 Math.pow 来做个简单的封装 Math.round(Math.pow(10, m) * number) / Math.pow(10, m),其中 number 是要四舍五入的数,m 是保留几位小数。
原码、补码、反码、移码计算方式
原码
原码就是未经更改的码,使用最高位表示符号位,正数为 0,负数为 1,剩下的数表示该数的绝对值。
例子:
机器字长为 8 位,由于最高位为符号位,所以能够表示的数值在 2^7 - 1 ~ -2^7 + 1
数字 127 表示为 0111 1111,数字 -127 表示为 1111 1111
反码
反码就是在原码的基础上,符号位不变,各位取反
例子:
数字 127 表示为 0000 0000,数字 -127 表示 1000 0000
补码
补码在原码的基础上,符号位不变,各位取反,末位加一
例子:
数字 127 表示为 0000 0001,数字 -127 表示 1000 0001
移码
将补码符号位取反即可
例子:
数字 127 表示为 1000 0001,数字 -127 表示 0000 0001
为什么 JavaScript 最大安全整数是 2^53-1
demo:
- 十进制数值:10.25
- 转化为二进制 => 1010.01
- 规格化 => 1.01001 * 2^3
- 存储: 01001 放在尾数位置,3 放在指数位。指数位有 11 位,则移码是 2^10+3,为 100…0011。整个表示为这样:

最大值
考虑能表示的最大值,就要看 1.x∗2^y 在固定位数时候所能表示的最大值。
指数位 移码最大值为 11 位 1,原码最大值为 10 位 1(原码最高位表示符号位),则 y 最大为 1023,x 最大表示 52 位 1。即 1.1111…1 乘以 2^1023,即 2^1023*(2-2^-52) 这个值也就是 Number.MAX_VALUE 的大小
最大安全整数
什么叫最大安全整数?指的也就是这个常量 Number.MAX_SAFE_INTEGER
现在考虑,我们看两个数 2^53 与 2^53+1。
2^53 我们尝试把它表示成二进制:1 53 个 0 ,规格化 1.0…00 * 2^53
那 2^53+1 呢?我们尝试把它表示成二进制:1 52 个 0 1 ,标准化 1.0…01 * 2^53
问题来了,尾数都有 53 位,但只要 52 个空! 它的处理办法是 忽略第 53 位 ,因此这两个数在计算机中表示的结果一样!
2 ** 53 === 2 ** 53 + 1; //true
此时就不安全了。显而易见,在 2^53-1 之后的数中,只要指数相同,并且尾数前 52 位相同,则这个两个数数值相同。
林秀栋的技术博客