基础数据类型-number知识摘要

213 阅读8分钟

关键词

BCD编码 科学计数法 IEEE 32和64位 符号,指数和分数 表示公式 定点数 浮点数 浮点数异常, 特殊值 +0 -0 ±∞ NaN 最小精度值 安全值 大数表示 进制转换(小数) 精度丢失 Kahan Summation 算法

定点数和浮点数的区别

约定计算机中小数点的位置,且这个位置固定不变,小数点前、后的数字,分别用二进制表示,然后组合起来就可以把这个数字在计算机中存储起来,这种表示方式叫做「定点」表示法,用这种方法表示的数字叫做「定点数」。 定点数如果要表示整数或小数,分为以下三种情况:

  1. 纯整数:例如整数100,小数点其实在最后一位,所以忽略不写
  2. 纯小数:例如:0.123,小数点固定在最高位
  3. 整数+小数:例如1.24、10.34,小数点在指定某个位置

对于整数 + 小数的情况,用定点表示时,需要约定小数点的位置,才能在计算机中表示。

不管如何约定小数点的位置,都会存在以下问题:

  • 数值的表示范围有限(小数点越靠左,整个数值范围越小)
  • 数值的精度范围有限(小数点越靠右,数值精度越低)

为了解决以上的问题,就出现了浮点数了,也就是小数点浮动的数字。 在现代计算机中,定点数通常用来表示整数,对于高精度的小数,通常用浮点数表示。

浮点数的「浮点」就是指,其小数点的位置是可以是漂浮不定的。而浮点数一般都是使用科学计数法来表示的,如十进制小数 8.345,用科学计数法表示,可以有多种方式:

8.345 = 8.345 * 10^0
8.345 = 83.45 * 10^-1
8.345 = 834.5 * 10^-2
...

由以上例子可以看出,浮点数中的小数点根据不同的表示情况,是浮动的。而浮点数的数字表示的公式则是

V = (-1)^S * M * R^E
  • S:符号位,取值 0 或 1,决定一个数字的符号,0 表示正,1 表示负
  • M:尾数,用小数表示,例如前面所看到的 8.345 * 10^0,8.345 就是尾数
  • R:基数,表示十进制数 R 就是 10,表示二进制数 R 就是 2
  • E:指数,用整数表示,例如前面看到的 10^-1,-1 即是指数

以JavaScript语言为例,其Number类型,使用IEEE754-2019规范的双精度浮点数(64位)来表示的,如下图所示:

20220424001554

其中符号位1bit,指数11bit,而尾数则为52bit,基数为2,表示二进制。

浮点数异常的场景

IEEE754定义了一下几种浮点数异常的情况:无效运算、除以零、溢出、下溢和不精确

  • 无效操作(EM_INVALID) 对于将要执行的运算,某个操作数无效。如负数平方根的操作数、Infinity/Infinity、使用NaN 操作数的任何运算, 都会输出一个NaN,表示浮点数异常。
Infinity/Infinity //NaN
Math.sqrt(-3) //NaN
Number(undefined) //NaN
  • 除以零(EM_ZERODIVIDE) 对于有限的非零 x,x / 0,如果x为正数,则输出Infinity, 为负数则输出-Infinity
3/0 //Infinity
-3/0 //-Infinity
  • 溢出(EM_OVERFLOW)
  • 下溢(EM_UNDERFLOW)
  • 不精确(EM_INEXACT)
  • 非规格化(EM_DENORMAL)

特殊值的浮点数表示

NaN

有一些算数操作是非法的,比如对负数开根号。这类非法操作被称为浮点数异常(floating-point exception),异常结果由特殊字符NaN(Not a Number)表示。 符号位(sign) = 0或1 有偏指数(biased exponent)= 所有位都是1 小数(fraction) = 除了所有位都是0的数(因为所有为0,表示无穷大)

小数位只要不全为0,就表示非数值。 0 11111111 11111111111100000010000(二进制) 或 1 11111111 11111111111100000010000(二进制)

-0

符号位(sign) = 1 有偏指数(biased exponent) = 0 小数(fraction)= 0

如下: 1 00000000 0000000000000000000000(二进制)

+0

符号位(sign) = 0 有偏指数(biased exponent) = 0 小数(fraction)= 0

如下: 0 00000000 0000000000000000000000(二进制)

Infinity

符号位(sign) = 0表示正无穷大。 有偏指数(biased exponent) = 所有位都是1 小数(fraction) = 所有位都是0.

如下: 0 11111111 00000000000000000000000(二进制)

-Infinity

符号位(sign) = 1表示父无穷大。 有偏指数(biased exponent) = 所有位都是1 小数(fraction) = 所有位都是0.

如下: 1 11111111 00000000000000000000000(二进制)

浮点数精度丢失问题和解决方案

浮点数精度丢失是使用IEEE754-2019规范中,将部分小数转换为二进制时会有无限循环而导致的精度问题,如0.1+0.2和0.3不相等。 解决方案有以下几种:

  • 使用最小精度值Number.EPSILON(推荐,考虑兼容性) Number.EPSILON表示1和Number可以表示的大于1的最小浮点数之间的差值,也就是2^-52,当最后的结果小于这个精度时,则表示相等。
x = 0.2;
y = 0.3;
z = 0.1;
equal = (Math.abs(x - y + z) < Number.EPSILON);
  • 使用外部库(推荐) 可以引入big.js来解决精度丢失的问题,如下
0.1 + 0.2                  // 0.30000000000000004
x = new Big(0.1)
y = x.plus(0.2)            // '0.3'
Big(0.7).plus(x).plus(y)   // '1.1'
  • 使用指定有效位数
let result = 0.1 + 0.2;
console.log(Number(result.toPrecision(16)) === 0.3)
  • 转换小数为整数再计算
function transToNum (n) {
    return n * 10;
}
let result = transToNum(0.1) + transToNum(0.2);
console.log(Number(result.toPrecision(16)) === transToNum(0.3))

大数的表示

在前端中,由于数字最大的值是 2^53 - 1,但是实际场景中会有超过这个数值的情况,因此新推出了BigInt内置对象,用于表示大于2^53 -1 的数字。 BigInt可以表示任意大的数字,可以在整数字面量后面增加一个n,如10n来定义,也可以通过BigInt构造函数来来构造,如下:

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n

使用 typeof 测试时, BigInt 对象返回 "bigint"。 以下操作符可以和 BigInt 一起使用: +、*-**% 。除 >>> (无符号右移)之外的 位操作 也可以支持。因为 BigInt 都是有符号的, >>> (无符号右移)不能用于 BigInt。为了兼容 asm.js ,BigInt 不支持单目 (+) 运算符。

BigInt 和 Number 不是严格相等的,但是宽松相等的。 Number 和 BigInt 可以进行比较,也可以混在一个数组内并排序。 BigInt 在需要转换成 Boolean 的时表现跟 Number 类似。 由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于2^53 时使用 BigInt 类型,并且不在两种类型之间进行相互转换。 对任何 BigInt 值使用 JSON.stringify() 都会引发 TypeError,因为默认情况下 BigInt 值不会在 JSON 中序列化。但是,如果需要,可以实现 toJSON 方法。 由于对 BigInt 的操作不是常数时间的,因而 BigInt 不适合用于密码学。

IE并不兼容BigInt,需要通过腻子脚本或者babel插件来支持。也可以通过引入big.js来解决前端的大数问题。

小数的二进制转换

二进制转换为十进制

转换的思路就是从右到左的第N位,乘上一个2的N次方,最后加起来就是十进制的结果。 比如 0011 这个二进制数,对应的十进制表示,就是 0×23+0×22+1×21+1×200 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 1 \times 2^0, 结果为3。

十进制转换为二进制

采用短除法,可以转换为二进制。通过将十进制的数除以2的余数,作为最右边的一位,然后用商继续除以2,把对应的余数紧靠着刚出算出 的余数的右侧,这样递归迭代,直至余数为0. 如下图:

余数二进制位
13/2611
6/2300
3/2211
1/2011

对应的二进制数就是1101

如果是有负数的情况,则通过补码的方式来解决,在最左侧的0或者1,来分别表示正数和负数的情况。

小数的转换逻辑

小数的二进制转换,是通过乘以2,看是否超过1,如果超过1,就记下1,并在结果中减去1,再进一步循环 操作,以0.1为例子:

20220424000144

0.1的二进制的最终结果会是0.0001100110011...永远没有尽头。这也是0.1 + 0.2 不等于 0.3的根本原因

原码,反码,补码,移码

  • 原码就是符号位和真值的绝对值组成的
  • 正数的补数是它自己,因此在补码中,正数的数值位是它自己,符号位则同原码一样,正数为0,负数为1
  • 反码,这种机器数的主要场景用于原码和补码的相互转换,反码作为中间数过度使用
  • 移码(又叫增码或偏置码)通常用于表示浮点数的阶码,其表示形式与补码相似,只是其符号位用“1”表示正数,用“0”表示负数,数值部分与补码相同

扩展

JavaScript的一个典型弱点就是位运算。JavaScript的位运算参照Java的位运算实现,但是Java位运算是在int型数字的基础上进行的,而JavaScript中只有double型(也就是双精度浮点数)的数据类型,在进行位运算的过程中,需要将double型转换为int型,然后再进行。所以,在JavaScript层面上做位运算的效率不高。

在应用中,会频繁出现位运算的需求,包括转码、编码等过程,如果通过JavaScript来实现,CPU资源将会耗费很多,因此node使用了C/C++扩展模块来解决位运算效率低问题。

参考资料

谈谈二进制-简书

IEEE754在线转换工具

big.js-github

BigInt-MDN

Number-MDN