一. 前置知识点
1. 十进制如何转为二进制?
整数部分除二取余数, 直到商为 0, 逆序排列, 小数部分乘 2 取整, 顺序排列, 直到积中小数部分为 0 或者到达要求精度.
8 转为二进制
8 / 2 = 4...0 取 0
4 / 2 = 2...0 取 0
2 / 2 = 1...0 取 0
1 / 2 = 0...1 取 1
二进制结果为: 1000
0.25 转为二进制
0.25 * 2 = 0.50 取 0
0.50 * 2 = 1.00 取 1
二进制结果为: 01
于是可得出 8.25 的二进制表示: 1000.01
复制代码
2. 二进制如何转为十进制?
注意: 二进制转为十进制不分整数部分与小数部分.
二进制 1000.01 转为十进制
1 * 2^3 + 0 * 2^2 + 0 * 2^1 + 0 * 2^0 + 0 * 2^-1 + 0 * 2^-2 = 8.25
复制代码
二. javascript 是如何保存数字的
JavaScript 里的数字是采用 IEEE 754 标准的 64 位 double 双精度浮点数
sign bit(符号): 用来表示正负号, 1 位 (0 表示正, 1 表示负)
exponent(指数): 用来表示次方数, 11 位
mantissa(尾数): 用来表示精确度, 52 位
对于没有接触的读者来说, 以上可能理解起来很模糊, 没关系, 接下来我们用案例具体说明其流程, 先看一下上述的十进制数 8.25 在 JS 中是如何保存的
十进制的
8.25
会被转化为二进制的
1000.01
;
二进制
1000.01
可用二进制的科学计数法 1.00001 * 2^4 表示;
1.00001 * 2^4 的小数部分 00001(二进制) 就是 mantissa(尾数) 了,
4
(十进制) 加上
1023
就是 exponent(指数) 了 (这里后面讲解为什么要加上 1023);
接下来指数
4
要加上
1023
后转为二进制
10000000011
;
我们的十进制
8.25
是一个正数, 所以符号为二进制表示为 0
8.25
最终的二进制保存
0-10000000011-0000100000000000000000000000000000000000000000000000
注意点:
不够位的我们都用 0 补充;
步骤 2 得出的科学计数中的整数本分 1 我们好像忘记, 这里因为 Javascript 为了更最大限度的提高精确度, 而省略了这个 1, 这样在我们我们本来只能保存 (二进制)52 位的尾数, 实际是有 (二进制)53 位的;
指数部分是 11 位, 表示的范围是 [0, 2047], 由于科学计数中的指数可正可负, 所以, 中间数为 1023,[0,1022] 表示为负,[1024,2047] 表示为正, 这也解释了为什么我们科学计数中的指数要加上 1023 进行存储了.
三. javascript 是如何读取数字的
我们还是以 8.25 的二进制 0-10000000011-0000100000000000000000000000000000000000000000000000 来讲述
首先我们获取指数部分的二进制
1000000001
, 转化为十进制为
1027
,
1027
减去
1023
就是我们实际的指数
4
了;
获取尾数部分
0000100000000000000000000000000000000000000000000000
实际是 0.00001(后面的 0 就不写了), 然后加上我们忽略的 1, 得出 1.00001;
因为首位为 0, 所以我们的数为正数, 得出二进制的科学计数为 1.00001 * 2^4, 接着再转为十进制数, 就得到了我们的
8.25
;
四. 从 0.1+0.2 来看 javascript 精度问题
这里就要进入我们的正题了, 看懂了前面的原理说明, 这部分将会变得很好理解了.
要计算 0.1+0.2, 首先计算要先读取到这两个浮点数
0.1 存储为 64 位二进制浮点数
没有忘记以上步骤吧~
先将 0.1 转化为二进制的整数部分为 0, 小数部分为
0001100110011001100110011001100110011...
咦, 这里居然进入了无限循环, 那怎么办呢? 暂时先不管;
我们得到的无限循环的二进制数用科学计数表示为
- 1.100110011001100110011001100110011... * 2^-4
- ;
指数位即是 - 4 + 1023 = 1019, 转化位 11 位二进制数
01111111011
;
尾数位是无限循环的, 但是双精度浮点数规定尾数位 52 位, 于是超出 52 位的将被略去, 保留
1001100110011001100110011001100110011001100110011010
最后得出 0.1 的 64 位二进制浮点数:
0-01111111011-1001100110011001100110011001100110011001100110011010
同上, 0.2 存储为 64 位二进制浮点数: 0-01111111100-1001100110011001100110011001100110011001100110011010
读取到两个浮点数的 64 为二进制后, 再将其转化为可计算的二进制数
0.1 转化为
- 1.1001100110011001100110011001100110011001100110011010 * 2^(1019 - 1023)
- --
- 0.00011001100110011001100110011001100110011001100110011010
- ;
0.2 转化为
- 1.1001100110011001100110011001100110011001100110011010 * 2^(1020 - 1023)
- --
- 0.0011001100110011001100110011001100110011001100110011010
- ;
接着将两个浮点数的二进制数进行加法运算, 得出 0.0100110011001100110011001100110011001100110011001100111 转化为十进制数即为 0.30000000000000004
不难看出, 精度缺失是在存储这一步就丢失了, 后面的计算只是在不精准的值上进行的运算.
五. javascript 如何解决精度问题出现的计算错误问题
对于小数或者整数的简单运算可如下解决:
- function numAdd(num1, num2) {
- let baseNum, baseNum1, baseNum2;
- try {
- baseNum1 = String(num1).split(".")[1].length;
- } catch (e) {
- baseNum1 = 0;
- }
- try {
- baseNum2 = String(num2).split(".")[1].length;
- } catch (e) {
- baseNum2 = 0;
- }
- baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
- return (num1 * baseNum + num2 * baseNum) / baseNum;
- };
复制代码
如: 0.1 + 0.2 通过函数处理后, 相当于 (0.1 * 10 + 0.2 * 10) / 10
但是如同我们前面所了解的, 浮点数在存储的时候就已经丢失精度了, 所以浮点数乘以一个基数仍然会存在精度缺失问题, 比如 2500.01 * 100 = 250001.00000000003, 所以我们可以在以上函数的结果之上使用 toFixed(), 保留需要的小数位数.
一些复杂的计算, 可以引入一些库进行解决.
来源: https://juejin.im/post/5b9c7cc6f265da0ab41e473b