0.1 + 0.2 = 0.30000000000000004
IEEE 754 标准
IEEE 754 是 IEEE 二进制浮点数算术标准的简称. 也是目前业界通用的浮点数运算标准. 其常用的浮点数值表示方法分为: 单精度(32 位) 和 双精度(64 位).
JavaScript 中不论小数还是整数都只用一种数据类型来表示, 即 Number 类型. 其遵循 IEEE 754 标准, 使用双精度浮点数(double)64(8 字节)来存储一个浮点数. 我们也只介绍 64bits 的数值存储.
64Bits 双精度是怎么存储数据的
在 IEEE 754 标准中, 任意一个二进制浮点数 V 可以通过以下形式表示:
- (-1)^S 表示符号位, 当 S = 0, V 为正数; 当 S = 1, V 为负数
- M 表示有效数字, 大于等于 1, 小于 2
- 2 ^ E 表示为指数
64Bits 分为以下 3 个部分
- sign bit (S, 符号): 用来表示正负号, 0 为正 1 为负 (1 bit)
- exponent (E, 指数): 用来表示次方数 (11 bits)
- mantissa (M, 尾数): 用来表示小数部分 (52 bit)

符号 S
用来存储正负号, 0 表示正数 (+), 1 表示负数 (-)
1 的二进制为 0000 0001
-1 的二进制为 1000 0001
尾数 M
IEEE 754 规定, 在计算机内部保存 M 时, 1 <= M < 2, 所以默认这个数的第一位总是 1, 因此可以被舍去, 只保存后面部分. 所以对于双精度 64 位浮点数, M 为 52 位, 但是可以保存的有效数字为 52 + 1 = 53 位.
所以 JavaScript 的最大安全数为 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1
1 | Math.pow(2, 53) === Math.pow(2, 53) + 1; // true |
尾数 E
E 为一个无符号整数, 在双精度浮点数中 E 为 11 位, 取值范围: 0 ~ 2047.
但是科学计数法中的 E 是可以出现负数的, 所以 IEEE 754 规定, E 的真实值必须再减去一个中间数. 对于 8 位的 E, 中间数是 127; 对于 11 位的 E, 中间数是 1023.
指数 E 还可以分成三种情况:
- E 不全为 0 或不全为 1, 这时, 浮点数计算方法是, 指数 E 的计算值减去 127 ( 或 1023), 得到真实值, 再将有效数字 M 前加上第一位的 1
- E 全为 0, 浮点数的指数 E 等于 1 - 127 ( 或 1 - 1023), 有效数字 M 不再加上第一位的 1, 而是还原为 0.xxxxx 的小数. 这样做是为了表示 ±0, 以及接近于 0 的很小的数字.
- E 全为 1, 如果有效数字 M 全为 0, 表示 ± 无穷大(正负取决于符号位 S), 如果有效数字 M 不全为 0, 表示这个数不是一个数 ( NAN )
十进制小数如何转二进制
转换
十进制小数转二进制, 小数部分 乘 2 取整数. 若乘之后的小数部分不为 0, 继续乘 2 直到小数部分为 0, 将取出的整数正向排序.
如: 0.1 转二进制
1 | 0.1 * 2 = 0.2 ---------------- 取整数 0, 小数 0.2 |
最终 0.1 的二进制表示为
1 | 0.000110011001100110011(0011); // 0011 将会无限循环 |
IEEE 754 存储
0.1 的二进制表示如下所示
符号位
由于 0.1 为整数, 所以符号位 S = 0
指数位
E = -4, 实际存储为 -4 + 1023 = 1019, 二进制为 1111111011, E 为 11 位, 最终值为 01111111011
尾数位
在 IEEE 754 中, 循环位不能无限循环下去, 在双精度 64 位下最多存储的有效整数位为 52 位, 采用就近舍入模式(进一舍零) 进行存储
1 | 11001100110011001100110011001100110011001100110011001; // M 舍去首位的 1,得到如下 |
得到最终结果:
1 | 0 01111111011 1001100110011001100110011001100110011001100110011010 |
0.1 + 0.2 等于多少?
通过同样的方法计算出 0.2 的存储值, 我们得到以下结果
1 | S E M |
浮点数运算步骤
对阶
浮点数加减首先判断两数的指数位是否相同(小数点位置是否对齐), 若两数指数位不同, 需要对阶保证指数位相同.
对阶时遵守小阶向大阶看齐原则, 尾数向右移位, 每移动一位, 指数位加 1, 直到指数位相同, 即完成对阶
本示例, 0.1 的阶码为 -4 小于 0.2 的阶码 -3, 故对 0.1 做移码操作
1 | // 0.1 移动之前 |
尾数右移 1 位之后最高位空出来了,如何填补呢?涉及两个概念:
- 逻辑右移:最高位永远补 0
- 算术右移:不改变最高位值,是 1 补 1,是 0 补 0
尾数求和
两个尾数直接求和
1 | 0 01111111100 1100110011001100110011001100110011001100110011001101 // 0.1 |
规格化
由于产生进位,阶码需要 + 1,对应的十进制为 1021,此时阶码为 1021 - 1023(64 位中间值)= -2,此时符号位、指数位如下所示
1 | S E |
尾部进位 2 位,去除最高位默认的 1,因最低位为 1 需进行舍入操作(在二进制中是以 0 结尾的),舍入的方法就是在最低有效位上加 1,若为 0 则直接舍去,若为 1 继续加 1
1 | 100110011001100110011001100110011001100110011001100111 = 00110011001100110011001100110011001100110011001101000 = 00110011001100110011001100110011001100110011001101000 = 0011001100110011001100110011001100110011001100110100; // + 1 // 去除最高位默认的 1 // 最后一位 0 舍去 // 尾数最后结果 |
IEEE 754 中最终存储如下:
1 | 0 01111111101 0011001100110011001100110011001100110011001100110100 |
最高位为 1,得到的二进制数如下所示:
1 | 2 ^ (-2 * 1.00110011001100110011001100110011001100110011001101); |
转换为十进制如下所示:
1 | 0.30000000000000004 |
如何解决精度问题
数字转成整数
1 | function add(num1, num2) { |
但是这种方法对数值较大的数支持依旧不好
用第三方库
总结
由于计算机底层存储都是基于二进制的,需要事先由十进制转换为二进制存储与运算,这整个转换过程中,类似于 0.1、0.2 这样的数是无穷尽的,无法用二进制数精确表示。JavaScript 采用的是 IEEE 754 双精确度标准,能够有效存储的位数为 52 位,所以就需要做舍入操作,这无可避免的会引起精度丢失。另外我们在 0.1 与 0.2 相加做对阶、求和、舍入过程中也会产生精度的丢失。