学习笔记—二进制和精度问题

日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。


二进制

所谓 二进制,就是只用 01 两个数字,在计数与计算时必须是 逢2进1 (如十进制的 10 就是二进制的 1010)

计算机中所有的内容全都是以 二进制 的形式进行存储的,而数据也都是以 二进制 的形式来表现的。

精度问题

参考文献 Floating Point Math

我们首先来思考一个问题,0.1 + 0.2 === 0.3 的输出结果是什么?

来看一下这道最经典的面试题。

1
console.log(0.1 + 0.2 === 0.3);

这道题的结果是 false ,我们通过控制台打印可以发现, 0.1 + 0.2 的结果并不是 0.3

1
console.log(0.1 + 0.2); // 0.30000000000000004

导致这个问题的原因就和计算机的 二进制存储 有关。

首先我们要先明确计算机数据存储的两个概念。

  • 计算机将所有数据以 二进制 的形式存储。
  • 计算机用 有限的大小 来存储数据。(因为现实生活中不存在无限大的内存或硬盘)

计算机中的 *最小单位是 *位(bit,比特) **。

1
2
3
4
5
6
7
8
9
10
11
- 8 bit => 1 byte;

- 1024 byte => 1 Kb;

- 1024 Kb => 1 Mb;

- 1024 Mb => 1 Gb;

- 1024 Gb => 1 T;

...

我们在进行数据存储时,一般以 字节 来进行存储。

字节(byte) 的最大值和最小值是 0~255 ,也就是说取值范围是 255

取值范围的 255 是因为计算机是通过 二进制来进行数据存储

进制的转换

二进制顾名思义就是 逢2进1,十进制就是 逢10进1

又因为 8bit = 1byte,所以他们之间相差 255。

二进制 转 十进制 其实就是 当前位的值乘以进制(2),次幂是当前所在位,也就是 1*2^n

具体计算代码如下。

1
2
3
4
5
let sum = 0;
for (let i = 0; i < 8; i++) {
sum += Math.pow(2, i)
}
console.log(sum); // 255

计算公式如下。
$$
∵ 12^n 且 n<8
∴ s = 1
2^0 + 12^2 + … + 12^7
$$

从 十进制 转 二进制,就是 当前数除以进制(2),并取余。然后将余数倒退,就是最终的结果。比如,5 的二进制就是 101

5除以2取余数

在代码中,我们一般使用 parseInt() 把 任意进制 转成 十进制。使用 toString() 把 十进制 转成 任意进制

  • 使用 0x 前缀表示 十六进制0o 前缀表示 八进制0b 前缀表示 二进制。如 0x64 表示 十进制的1000o555 表示 十进制的3650b1111 表示 十进制的15。
  • toString() 如果不填入参数,则默认转换成 十进制。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 任意进制 转成 十进制
console.log(parseInt('20',10)); // 20
console.log(parseInt('11',2)); // 3
console.log(parseInt('20',16)); // 32
// 十进制 转成 任意进制
console.log((3).toString(2)); // 11
console.log(3..toString(2)); // 11
console.log((77).toString(8)); // 115
console.log((77).toString(16)); // 4d
console.log((17).toString(8)); // 21
// 十六进制 转成 任意进制
console.log((0x64).toString(2)); // 1100100
console.log((0x32).toString(8)); // 62

小数转换

再回到我们最上面的题目。

我们现在需要算的是 小数(浮点数),那么我们将小数转换成二进制进行存储,我们就需要用到 乘二取整法

将 小数(浮点数)乘以2,若计算结果包含整数,则取出整数,记为1。反之则记为0。

0.5 * 2 = 1 …… 0 ,则 0.5 的二进制为 0.1

现在我们来看一下, 0.1 的二进制是多少。

1
2
3
4
5
6
7
8
// 乘二取整法
// 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.0001100110011001100110011001100110011001100110011001101

同理我们也可以算出,0.2 的二进制是 0.001100110011001100110011001100110011001100110011001101

我们可以发现,0.10.2 的二进制差了一位,无线数在计算机内部存储时会有长度限制,所以最终会存在错位的情况。

所以其十进制结果,最后会有几个 二进制单位的1 无法被计算。

这样才会导致 0.1 + 0.2 === 0.30000000000000004

精度问题的解决方案

推荐使用 Math.js 进行处理,他支持 BigNumber 的数据类型。

1
2
3
4
const math = require('mathjs');
let big1 = math.bignumber(0.1);
let big2 = math.bignumber(0.2);
console.log(math.add(big1, big2)); // 0.3

本篇文章由 莫小尚 创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
最后感谢您的支持!

请打赏并支持一下作者吧~

欢迎关注我的微信公众号