Javascript浮点数
有一道很常见的面试题
0.1 + 0.2 === 0.3 // true ? false大家应该都知道是 false,但是为毛不相等呢?下面我将从 浮点数表示 及 浮点数精度 两个方面来解释
Javascript 浮点数表示
Javascript 中不存在整型和浮点型之分,只有一个类型 Number,它遵循 IEEE 二进制浮点数算术标准(IEEE754),使用 64 位 双精度浮点数(double) 存储。
双精度存储是知道了,那双精度浮点数是如何存储数据呢?主要有以下几个关键点
1. 双精度浮点数使用 64 位存储

- sign(S):符号位,长度为 1,0 代表数字为正,1 代表数字为负
- exponent(E):指数位,长度为 11,二进制科学计数法的指数位
- mantissa(M):尾数位,长度为 52 ,二进制科学计算法的尾数位
数值的计算公式为(二进制):
2. 使用二进制科学计数法
举个栗子: 转换为二进制表示 转换为二进制科学计数法
3. 指数位表示有符号整数
因为指数位数值为无符号整数,范围为[0, 2047],在规格化值中取 [1, 2046]。但指数位需要表示的是有符号整数,则[1, 1022] 表示为负数[-1022, -1],1023 表示为 0,[1024, 2046]表示为正数[1, 1023],所以整个指数位表示的范围是[-1022, 1023]
由上可推导出公式:
- E:指数位表示的数值
- e:指数位实际存储的数值
举个栗子: 表示为二进制科学计数法,该数 E = -1,e = 1022
tips: 下面会讲什么是规格化值,和其他的值
4. 尾数不表示二进制科学计算的整数部分,即不表示 1
由于规格化的数整数位都是 1,所以在存储时可以节约空间,不表示整数位,从小数位开始表示。所以尾数位其实最大能表示 53 位
由上可推导出公式:
- M:位数为表示的数值
- f:位数为实际存储的数值
举个栗子: 二进制科学计数法,该数 M=1,则实际存储 f=0,
由上可将数值计算公式推导为
非规格化值 和 特殊值
那么问题来了,如果按公式 ,那么如何表示 0 呢?,即使你设 f = 0,e=任意值,按照公式算出,value 不可能为 0。当然第一个公式 没有问题,问题出在公式的推导,因为推导出的公式只符合规格化数值,下面介绍下规格化数值和其他数值
- 规格化数值:e 不全为 0,也不全为 1,f 为任意值
- 非规格化数值:e 全部为 0,f 为任意值。非规格化数值主要用于表示 0,以及接近 0 的数。此时公式为
- 无穷大:e 全部为 1,f 为 0
- NaN:e 全部为 1,f 不为 0
所以:当 e = 0 时,f = 0 时,表示数值 0
解释 0.1 + 0.2 !== 0.3
到这里,我们已经可以解释 0.1 + 0.2 为什么不等于 0.3 咯
表示为二进制为:
转换为二进制科学计数法为:
计算得:S = 0,E = -4,M = 1.1001100…1100…1100…,则 S = 0,e = 1029,f = 1001100…1100…11010
将截断(舍入)后的数值重新表示为二进制,则 0.1 最终的二进制数值为:
0.00011...0011...001101
同理,表示出 0.2,0.3 的二进制数值
0.1:0.00011001100110011001100110011001100110011001100110011010.2:0.0011001100110011001100110011001100110011001100110011010.3:0.010011001100110011001100110011001100110011001100110011
0.1 + 0.2 和为: // 下面会详情介绍该步骤 0.0100110011001100110011001100110011001100110011001101将和与0.3对比,发现并不相等,中间差值为: 0.000000000000000000000000000000000000000000000000000001从结果值来看,中间差值已经很小很小了,已经可以忽略了不计了。事实上在 ES6 Number 扩展中,增加 Number.EPSILON 属性,表示 1 与大于 1 的最小浮点值差,值为 ,当值小于 Number.EPSILON 时,一般可忽略不计
浮点数精度
舍入
尾数位只能存储 52 位,但是在 0~1 之间的实数是无穷尽的,这些无穷的数该如何表示呢?既然完全表示不可能完成,那么只有舍弃掉某些数值,来找出最近的浮点数匹配。那么到底采用哪种舍入方法呢?下面介绍常用的舍入方法
- 向偶数舍入:也称为向最接近值舍入
- 向零舍入:舍弃末尾位以外的数值,可以按照
Math.trunc理解 - 向正无穷舍入:向较大的数舍入,可以按照
Math.ceil理解 - 向负无穷舍入:向较小的数舍入,可以按照
Math.floor理解
IEEE754 采用的是 向偶数舍入,原则是保证损失精度最小,下面简单介绍一下舍入规则
- 对于恰巧中间值的情况,如果保留位数最后一位是偶数则舍弃后续数值,奇数则进位。例如 保留 1/2 为 , 保留 1/2 为
- 对于向上的值较近,则进位。如 保留 1/2 为
- 对于向下的值较近,则舍弃。如 保留 1/2 为
举个栗子:
0.1 => 0.0001100110011001100110011001100110011001100110011001100 | 110011...// 由于需要保留的最后一位数后为 110011...,舍入时离向上的值较近,应该进位,所以0.1 => 0.0001100110011001100110011001100110011001100110011001101- ”|” 表示在该处需要舍入
tips:如果你实在无法判断如何舍入,有个简单的办法。先向下舍入,与原数相减;再向上舍入,与原数相减,将两个差值比较,取差值绝对值较小的那个数;如果差值相等,则取末尾为偶数的那个数
计算
为了进一步保证精度,IEEE754 标准,双精度在中间计算时,额外保留三位,分别是 保护位、舍入位、粘贴位
- 保护位:精度最低位右侧一位(双精度可以理解为尾数的第 53 位)
- 舍入位:保护位右侧一位
- 粘贴位:舍入位右侧一位,代表舍入位右侧是否还有数据,如果右侧还有数据,则粘贴位为 1,否则为 0,目的是为了支持目标数值向最近的偶数舍入
在浮点数计算时,通过额外保存三位,来增加计算的正确性,找到浮点数最接近的匹配。
模拟 0.1 + 0.2 计算
讲了那么多,感觉没什么用,最后还是简单写一下 0.1 + 0.2 的二进制计算过程。
0.1:S = 0,E = -4,M = 1.10011001100110011001100110011001100110011001100110100.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010
// 对阶 小阶对大阶0.1:S = 0,E = -3,M = 0.110011001100110011001100110011001100110011001100110100.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010
// 将 M 值相加和为:10.01100110011001100110011001100110011001100110011001110
计算出的0.3:S = 0,E = -3,M = 10.01100110011001100110011001100110011001100110011001110
// 规格化计算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011 | 10
// 计算时,右边多保留两位,此处保护位 = 1,舍入位 = 0,粘贴位 = 0// 舍入后计算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110100浮点数的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011
// 转换10进制计算出的0.3 = 0.30000000000000004- ”|” 表示应该截断,后续数值即为保护位,舍入位,粘贴位
参考
- 《深入理解计算机系统》第二章节
- 《计算机组成与设计 软件/硬件接口》 第三章节
- 维基百科 IEEE 754