logo

javascript中的数值奥秘

关于 IEEE 754

IEEE 是 Institute of Electrical and Electronics Engineers 的缩写,即电气电子工程师学会,是一个工业标准开发者,制定相关领域的标准的机构。
IEEE 754 就是由其开发制定的浮点算法规范,javascript 中数值采用该标准,像 NaNInfinity/-Infinity+0/-0 等等这些特殊值就是属于该规范的一部分。
不仅仅是 javascript 采用了该标准,C,Java,Python 等程序语言同样采用该标准表示数值。

如何表示一个数

IEEE 754 中其实制定了四种不同 bit 下的数值表示方法(32bit,64bit,43+bit,79+bit),其中 32bit,64bit 也被称之为单精度浮点数双精度浮点数
javascript 采用了 64bit 的表示方法,也就是说在 javascript 中的数值都是使用 64 位浮点数进行存储运算。
64bit 的数值表示结构如下图:

一个 64 位浮点数由 1 位 sign(符号位),11 位 exponent(指数位),52 位 fraction(有效位数)组成。
通过以下公式我们可以表示出一个数:

(-1)^S * 2^(Exp-bia) * 1.(Fraction)

注:bia 表示一个偏移值

ps:从公式我们可以得出最大安全整数,为Fraction部分全为1,即1.11111…(52位),小数点向右移动52位即为最大安全整数,Exp-bia = 52时的值为 2^53-1。

符号位

(-1)^0 => 1,表示非负数。(-1)^1 = >-1,表示非正数。

指数偏移值

指数部分采用无符号位的表示方法,因此需要一个偏移值计算出真正的值,bia就是这个偏移值。
11 位二进制表示的最大数为 2^11-1,即 0 ~ 2047。标准规定偏移值固定为 2^(bits-1)-1,我们可以得出偏移值bia为 2^(11-1) - 1 => 1023,实际表示的指数区间范围为[-1023,1024]。

例如:存储的指数部分为 0,实际值为 0 - 1023 => -1023。存储值为 2047,实际值为 2047 - 1023 => 1024。

指数对应值

标准规定了不同的指数范围表示不同的值。
|e|e-bia|说明|
|—|—|—|
|0|-1023|+0/-0 或者 非标准浮点数|
|[1,2046]|[-1022,1023]|通常浮点数|
|2047|1024|+∞/-∞ 或 NaN|

指数 e 与小数 f 组合对应的值

f\e0[-1022,1023]1024
f=0(-1)^S * 0
=>
+0/-0
(-1)^S * 2^(e-1023) * 1(-1)^S * ∞
=>
+∞/-∞
f!=0(-1)^S * 2^(-1023) * (1.f)
=>
(-1)^S * 2^(-1022) * (0.f)
(-1)^S * 2^(e-1023) * (1.f)NaN

0.1+0.2 为何不等于 0.3

实际上浮点数的比较通常会附加一个偏差值,如果在偏差值以内可以看做相等的两个值,而不是直接使用等于进行判断。
这个问题我们可以通过具体计算一步步推出 0.1+0.2 的计算结果。
将 0.1 与 0.2 转换为 64 位浮点数表示如下:

1
2
0.1: (-1)^0 * 2^(-4) * 1.(1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010)
0.2: (-1)^0 * 2^(-3) * 1.(1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010)

注:具体计算搜索乘2取整

浮点数计算过程中,将较小的指数化为较大的指数,对 0.1 进行升幂,整体向右移动一位(0.1 最后的有效位数 0 被舍去):

1
2
0.1: (-1)^0 * 2^(-3) * 0.(1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101)
0.2: (-1)^0 * 2^(-3) * 1.(1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010)

将两数相加得到:

1
0.3: (-1)^0 * 2^(-3) * 10.(0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111)

这里转为标准写法时,全部右移一位,末尾 1 被舍去并向前进一位

1
2
0011 + 0001 = 0100
0.3: (-1)^0 * 2^(-2) * 1.(0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100)

验证:

1
2
3
4
5
6
0.1 + 0.2 ===
(-1) ** 0 *
2 ** -2 *
(0b10011001100110011001100110011001100110011001100110100 * 2 ** -52);
//true
//**求幂运算符为ES2017提案

javascript 中的特殊值

javascript 重定义一系列特殊的常数值,我们可以试着使用 64 位浮点数表示出来并验证。

Number.MAX_VALUE/Number.MIN_VALUE

IEEE 754 中 64 位浮点数表示的最大值/最小值为,指的是最大正数和最小正数,需要对应负数改变符号位即可:

1
2
3
4
5
最大:
(-1)^ 0 * 2^1023 * 1.(111111...)
符号位为0,指数位1023,小数都为1(二进制数)
最小:
(-1)^ 0 * 2^-1022 * 0.(000000...1)

验证:

1
2
3
4
5
6
Number.MAX_VALUE ===
(-1) ** 0 * 2 ** 1023 * (parseInt("1".repeat(53), 2) * 2 ** -52);
//true
Number.MIN_VALUE ===
(-1) ** 0 * 2 ** -1022 * (parseInt(`${"0".repeat(52)}1`, 2) * 2 ** -52);
//true

Number.MAX_SAFE_INTEGER/Number.MIN_SAFE_INTEGER

最大的安全整数指的是不损失精度的前提下最大能表示的整数,这是一对相反数,由于小数部分最多 52 位,所以指数最大为 52,因此最大值表示如下:

1
(-1)^0 \* 2^52 \* 1.(1111....)

验证

1
2
Number.MAX_SAFE_INTEGER ===
(-1) ** 0 * 2 ** 52 * (parseInt("1".repeat(53), 2) * 2 ** -52);

Number.EPSILON

一个极小值,如果计算结果与期望结果相差在这个数值的范围内,则可以认为结果与期望值相等。

1
2
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON;
//true

这个数可以表示为:

1
(-1)^0 \* 2^0 \* 1.(000...001) - 1

验证:

1
2
3
Number.EPSILON ===
(-1) ** 0 * 2 ** 0 * (parseInt(`1${"0".repeat(51)}1`, 2) * 2 ** -52) - 1;
//true

parseInt(0.0000008) === 8

要解释这个问题先要了解一些 javascript 的默认行为。

  • 如果一个数小于 1 而且小数点后紧接着超过五个 0 会自动转换成科学计数法。
  • 如果一个数小数点前的数值超过了 20 位会自动转换成科学计数法
1
2
0.0000001; //1e-7
1234567890123456789012; // 1.2345678901234568e+21

当 parseInt接受一个字符串,如果传入数值为数值类型就会隐式转换成字符串8e-7=>’8e-7’,
又因为 parseInt 碰到非数字会停止因此 parseInt(0.0000008)等同于 parseInt(‘8e-7’)最终得到相等答案。

类似的我们还可以这样:

1
2
3
parseInt(1000000000000000000000) === 1;
//1000000000000000000000 => 1e+21 => '1e+21' => 1
//true

参考链接:

Tags: IEEE 754