JS 认识浮点数 - 从计算机到JS
科学计数法
很多编程教材和百度出来的文章都会这样说:“计算机中遵循IEEE754规范,小数点飘忽不定的数就是浮点数。”这确实是句废话,你小数点“飘忽不定”,请问“飘忽不定”是怎么体现的,然后背后其原理又是如何的呢?这些被笼统描述甚至不阐述就导致了读者一知半解。
接下来我们利用科学计数法对浮点数开展学习:
数学中科学计数法的表达:
8.355 = 8.355 *10 ^0
8.355 = 83.55 *10 ^-1
8.355 = 835.5 *10 ^-2
8.355 = 8355 *10 ^-3
83550000000 = 8.355 *10 ^10
计算机中科学计数法的表达:
8.355 === 8.355e0
8.355 === 83.55e-1
8.355 === 835.5e-2
8.355 === 8355e-3
83550000000 === 8.355e10
顺理成章的我们将以上的科学计数法总结出一个通用表达式:V = (-1)^S * M * R^E
S:符号位,取值 0 或 1,决定一个数字的符号,0 表示正,1 表示负。
M:尾数,用小数表示,例如前面所看到的 8.355 * 10^0,8.355 就是尾数。
R:基数,表示十进制数 R 就是 10,表示二进制数 R 就是 2。
E:指数,用整数表示,例如前面看到的 10^-1,-1 即是指数。
计算机运算中浮点数的设计
从例子出发
接下来我们参考 科学计数法 自定义一些规则,看看如何在32bit中表示一个浮点数。
如图所示,我们定义了如下规则:
- 符号位 S 占1bit
- 指数 E 占10bit
- 尾数 M 占21bit
(D)
代表十进制,(B)
代表二进制
现在我们将12.125转换为32bit浮点数。
整数:12(D) = 110(B)
小数:0.125(D) = 0.001(B)
所以:12.125(D) = 110.001(B) = 1.10001 *2 ^2(B)
按自定义规则排列得到:0 1100000000 110001000000000000000
注意: 以上示例不是真正标准的浮点数排列,而是为了举例子自定义的。
总结
当我们指定的 指数位数 越大时,浮点数的范围就越大,反之越小。同理,尾数位数 越大时浮点数精度就越高,反之精度就越低。
早期人们提出浮点数定义时,就是这样的情况,当时有很多计算机厂商,例如IBM、微软等,每个计算机厂商会定义自己的浮点数规则,不同厂商对同一个数表示出的浮点数是不一样的。
这就会导致,一个程序在不同厂商下的计算机中做浮点数运算时,需要先转换成这个厂商规定的浮点数格式,才能再计算,这也必然加重了计算的成本。
那怎么解决这个问题呢?业界迫切需要一个统一的浮点数标准。
浮点数统一标准
直到1985年,IEEE
组织推出了浮点数标准,就是我们经常听到的IEEE754
浮点数标准,这个标准统一了浮点数的表示形式,并提供了 2 种浮点格式:
- 单精度浮点数 float:32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit
- 双精度浮点数 float:64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit
为了使其表示的数字范围、精度最大化,浮点数标准还对指数和尾数进行了规定:
- 尾数 M 的第一位总是 1(因为二进制的数只有 1 和 0),因此这个 1 可以省略不写,它是个隐藏位,这样单精度 23 位尾数可以表示了 24 位有效数字,双精度 52 位尾数可以表示 53 位有效数字
- 指数 E 是个无符号整数,表示 float 时,一共占 8 bit,所以它的取值范围为 0 ~ 255。但因为指数可以是负的,所以规定在存入 E 时在它原本的值加上一个中间数 127,这样 E 的取值范围为 -127 ~ 128。表示 double 时,一共占 11 bit,存入 E 时加上中间数 1023,这样取值范围为 -1023 ~ 1024。
针对特定情况,还有以下规定:
- 指数 E 全 0,尾数非 0:非规格化数,尾数隐藏位不再是 1,而是 0(M = 0.xxxxx),这样可以表示 0 和很小的数
- 指数 E 全 1,尾数全 0:正无穷大/负无穷大(正负取决于 S 符号位)
- 指数 E 全 1,尾数非 0:NaN(Not a Number)
回到开篇的例子,我们用统一标准将12.125转换为32bit浮点数看看。
整数:12(D) = 110(B)
小数:0.125(D) = 0.001(B)
所以:12.125(D) =110.001(B) = 1.10001 *2 ^2(B)
指数:2 + 127 = 129(D) = 10000001(B)
按统一标准排列得到:0 10000001 10001000000000000000000
计算机中浮点数的精度和范围
精度主要由尾数位数决定
用 乘二取整,顺序排列 的方法计算 0.2(D) 的二进制数。
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.2(D) = 0.00110011...(B)
可以看出 十进制数0.2 的二进制数是 无限循环 二进制小数。但由于单精度尾数位数的限制,所以只会保留到小数点后面23位。这就造成了精度上的损失。
精度主要由尾数(十进制)位数决定,单精度23bit尾数,也就是说能够精确到小数点后面23位1.00000000000000000000001(二进制)
前面的1是固定隐藏数位。
log10(2 ^(-23)) ≈ 1.19 *10 ^(-7)
所以单精度浮点数的最大精度为 7位,即精确到小数点后 7位。
同理,双精度浮点数的最大精度为 16位,即精确到小数点后 16位。
Tips:最大精度位数即是有效位数。
范围主要由指数位数决定
32bit 单精度浮点数 有效范围 大约是:
1.111...111 *2 ^127(B) ≈ 2 ^128(D) ≈ 3.4 *10 ^38(D)
十进制:-2 ^128 < 2 ^128
十进制:-3.4 *10 ^38 < 3.4 *10 ^38
同理,64bit 双精度浮点数 有效范围 大约是:
十进制:-2 ^1024 < +2 ^1024
十进制:-1.79 *10 ^308 < 1.79 *10 ^308
JS中的浮点数(2022.7.4)
在JS中存储浮点值使用的内存空间是存储整数值的两倍,所以JS核心ECMAScript
总是想方设法把值转换为整数。
let num1 = 1., num2 = 1.0; // num1和num2都会被作为整数 1 处理
console.log(num1, num2) // 1, 1
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。默认情况下,ECMAScript
会将小数点后至少包含 6 个零的浮点值转换为科学记数法。
let num = 0.0000001;
console.log(num); // 1e-7
与计算机运算中浮点数精度不一样,JS中浮点数精度(有效位数)最高保留 17位(十进制)
console.log(0.1 + 0.2); // 0.300 000 000 000 000 04