本文是学习笔记,可能会有不严谨之处。
零、指数运算符(或求幂赋值运算符)
-
两个星号
**
是 ES6 新增的求幂赋值运算符,操作符右边为指数,左边为底数2 ** 2; // 4
-
多个指数连用时,从最右边开始计算,即右结合
2 ** 3 ** 2 === 2 ** (3 ** 2); // true
-
与等号结合
let a = 2; a **= 2; // a = a ** 2
-
V8 引擎的指数运算符与 Math.pow 的实现不相同,特别大的运算结果会不相同 -
V8 应该已经修复了结果不一致的问题
Math.pow(299, 299) === 299**299
一、原码、反码和补码
1. 机器数
机器数:将符号"数字化"的数,是数字在计算机中的二进制表示形式
-
符号数值化
- 用一位二进制的 0 或 1 区分实际数的正负
- 通常这个符号放在二进制数的最高位,称符号位,以 0 代表“+”,以 1 代表“-”
- 因为符号位的存在,数的形式值不再等于真正的数值
-
二进制的位数受计算机机器字长的限制
- 机器内部设备一次能表示的二进制位数叫机器的字长
- 机器字长一般是字节(Byte)的整倍数,如:16 位,32 位等。一个字节是 8 位字长(
1Byte = 8bit
)
// 实数 +3,计算机的字长8位 0 0000011 // 实数 -3,计算机的字长8位 1 0000011
2. 真值
-
因为机器数第一位是符号位,所以机器数的形式值就不等于真正的数值
-
将带符号位的机器数对应的真正数值并加上符号
+
或-
称为机器数的真值0000 0001
的真值为+000 0001
3. 原码
-
原码就是不做更改的机器数
-
表示有符号数时,可以表示的范围
-(2n-1-1) ~ +(2n-1-1)
- n 为计算机的字长,例如 8 位,则原码表数范围
-127~+127
- 在原码表示中存在
+0 (00000000)
和-0 (10000000)
,所以 256 个数
- n 为计算机的字长,例如 8 位,则原码表数范围
-
原码可以表示无符号数,即最高位也是数值,不存在符号位(反码和补码则不能,因为反码补码肯定有符号位)
0 ~ 2n - 1
- n 为计算机的字长,例如 8 位,则表数范围 0~255
-
原码不能直接参与运算,因为符号位的存在
// 数学上 1 + (-1) = 0 // 二进制原码 00000001 + 1000001 = 100000010 // 10000010 转为十进制是 -2
4. 反码
-
正数的反码和原码相同
-
负数的反码,符号位保持为
1
,数值位根据原码中的数值按位取反 -
反码中存在多余的负零和其他问题,所以减法运算使用补码而不是反码
-
反码系统的表数范围和原码一样,存在
+0
和-0
-(2n-1-1) ~ +(2n-1-1)
5. 补码
-
计算机系统中,数值一律用补码来表示和存储
- 正数的补码和原码相同
- 负数的补码,符号位保持为
1
,数值位根据原码中的数值按位取反(反码),最后再加上 1,不考虑溢出比特
-
补码系统可以在加法或减法处理中,不需因为数字的正负而使用不同的计算方式
-
只要一种加法电路就可以处理各种有号数加法,减法可以用一个数加上另一个数的补码来表示
// 实数 -3 // 原码 1,000 0011 // 反码 1,111 1100 // 补码 1,000 1101 <!-- 用逗号将符号位和数值分开 -->
-
简单来说,数字a(无论正负)的补码就是-a(除0 和最小负数)
-
补码的表示形式中,0 只有一种形式,但是有该字长内可表示的最大的负值。例如 8 位字长,表示范围则为
-128~127
-2n-1 ~ +(2n-1-1)
a. 特别的数字: 0 和最小负数(补码等于本身)
下面都以 8 位为例
-
字长 8 位,则补码可以表示的最小负数为
-2**(8-1) = -128
-
0
// 原码 0,000 0000 // 反码 0,111 1111 // 补码 0,111 1111 + 1 = 0,000 0000(溢出位忽略)
-
-128
// 原码 1,000 0000 // 反码 1,111 1111 // 补码 1,111 1111 + 1 = 1,000 0000
b. n 位(bit) 的补码系统中几个特殊值
补码 | 实际数字 | 含义 |
---|---|---|
0 111...111 | 2n-1-1 | 有符号位的最大正数 |
0 000...000 | 0 | 补码中只有这一种 0 的表示形式 |
1 000...001 | -2n-1+1 | 比如是 8 位的系统,但是现在 n 取 5 位 |
1 000...000 | -2n-1 | 比如 8 位的补码系统,这个值就表示-128(最小的负数) |
c. 补码的工作原理
-
用钟表作为例子,假设当前时针指向 10 点,将时针顺时针拨动 8 个小时,指向 6 点,或者将时针逆时针拨动 4 个小时,也指向 6 点
- 上述钟表就是一个以 12 模的系统
- 8 和 4 互为补数,上面的操作,凡是减 4 操作都可以用加 8 来代替。特点就是两个数值想加等于模
-
模指记数范围,n 位的计算机的计量范围 0~2n-1,模 = 2n。“模”实质上是计量器产生溢出的量
- 假设
n=8
位的计算机 - 表示的最大数是
11111111
,若再加 1,即11111111+1=100000000
,溢出的位舍去,又回到00000000
,所以 8 位二进制系统的模为 2**8=256 - 模 256 下的加减法,用
0,1,...,254,255
表示值和用-128,-127,...,0,...,127
等价。例如,(126+125) ≡ 251 ≡ -5 (mod 256)
- 假设
-
机器中的二进制数
- 在 8 位硬件中
1000 0011
是实际存在的一个电气状态 - 给这个状态起一个名字叫+131,也可以给它起个名字叫-3
- 如果叫+131,那么结合实际数字,可以将
00000000
到11111111
起上相应的名字 0 到+255 - 如果叫-3,可以给这些的状态定义成符合某中规律的名字含义。例如补码系统中,根据 a 的补码等于-a,可以发现用最高位表示符号后,256 个状态可以拆分成两组,即-128~-1 和 0~127
- 如果叫+131,那么结合实际数字,可以将
- 在 8 位硬件中
-
结论:
- 一个负数可用它的正补数(模加上负数本身得到的数)代替
- 互为补数的两个数的绝对值之和为模
- 正数的补码等于该正数的原码
d. 定点小数的补码
-
1 > x ≥ 0
,[x]补 = xx = 0.1001 [x]原 = [x]补 = 0.1001
-
0 > x > ≥ -1 (mod 2)
,[x]补 = 2 + xx = -0.0110 [x]补 = 2 + x = 10.0000 - 0.0110 = 1.1010
e. 定点加减运算
- 减法运算可以看做被减数加上一个减数的负值,所以采用补码作加减运算
- 基本公式
- 整数:[A]补 + [B]补 = [A+B]补 (mod 2n+1)
- 小数:[A]补 + [B]补 = [A+B]补 (mod 2)
- 对于减法 [A-B]补 = [A + (-B)]补
- 整数:[A-B]补 = [A]补 + [-B]补 (mod 2n+1)
- 小数:[A-B]补 = [A]补 + [-B]补 (mod 2)
A = 0.1011, B= -0.0101
[A]补 = 0.1011, [B]补 = 1.1011
[A+B]补 = 0.1011
+ 1.1011
= 10.0110
-> 纯小数的补码模mod为2,也就是说整数不能超过1,否则溢出
-> 符号位的进位舍去,即最高位1舍去
[A+B]补 = 0.0110
-
判断溢出
- 使用一位符号位判断,无论加减,如果两数的符号相同,但是运算结果的符号和这两个数的符号不同,则为溢出
- 使用两位符号位(此时 mod 不再是 2,而是 4)判断是否溢出,当两位符号位不同,则溢出,否则无溢出,无论是否溢出,最高位表示真正的符号
x = -0.1011; y = -0.0111 [x]补' = 11.0101; [y]补' = 11.1001 [x]补' + [y]补' = 11.0101 + 11.1001 = 110.1110
- 符号位的进位丢掉,就是最左边的 1,符号位只有两位
- 结果的符号位
01
表示正溢出,10
表示负溢出 - 第一个符号位表示实际的符号
6. 移码
-
将补码的符号位取反得到(仅符号位相反)
-
一般用做浮点数的阶码 E,阶码 = 移码 - 1
(00000011)[原] = (01111101)[补] = (11111101)[移]; (10000011)[原] = (11111101)[补] = (01111101)[移];
二、 进制转换
整数
0. 基础
- 基数:每个进制的基数 R。二进制就是 2,十六进制是 16
- 权值:固定位置上的计数单位。例如,
2**2
,2**1
,2**0
- 位权:多位数,处在某一位上的“1”所表示的数值的大小,称为该位的位权
- N 进制数,整数部分第 i 位的位权为
N**(i-1)
。2 进制整数第 4 位为2**(4-1)
- N 进制数,小数部分第 j 位的位权为
N**(-j)
- N 进制数,整数部分第 i 位的位权为
1. 十进制转为二进制(除 2 取余法,逆序读取)
- 每次将整数部分除以 2,余数为该位权上的数
- 商如果不为 0,则继续做除以 2 的操作,直到商为 0
- 从最后一个余数开始读,直到第一个余数
2 | 23
|________
2 | 11 ···· 1
|_______
2 | 5 ···· 1
|_______
2 | 2 ···· 1
|______
2 | 1 ···· 0
|____
0 ···· 1
2. 二进制转十进制
- 根据权值,计算该位的值,所有位的值想加
- 从 0 开始,第 0 位
X0 * 2**0
,第 1 位X1 * 2**1
…… 最后所有值想加
( 1 * 24 ) + ( 0 * 23 ) + ( 1 * 22 ) + ( 1 * 21 ) + ( 1 * 20 ) = 23
3. 八进制和十六进制
- (8 或 16 ----> 10)八进制或十六进制转十进制同二进制,将 2 改成 8 或者 16 即可
- (10 ----> 8 或 16)十进制转为八进制或十六进制,可以使用除 8 取余或除 16 取余
- 计算量小的方法:先将十进制转为二进制,再通过二进制转为八或十六进制
010111
(2 7)8
00010111
(1 7)16
小数
1. 十进制转二进制(乘 2 取整法,顺序读取)
- 用 2 乘十进制小数得积,取积的整数部分为该位的位权上的数
- 取整后余下的小数部分再乘 2 取整,直到积中的小数部分为 0(或达到所要的精度)
- 将整数部分按顺序读取
0.8125 x 2 = 1.625 ···· 1
0.625 x 2 = 1.25 ···· 1
0.25 x 2 = 0.5 ···· 0
0.5 X 2 = 1.0 ···· 1
2. 二进制转为十进制
- 根据权值,计算该位的值,所有位的值想加
- 从小数点后第 1 位开始,小数点后第 1 位
X0 * 2**-1
,小数点后第 2 位X1 * 2**-2
……Xn * 2**-n
最后所有想加
( 1 * 2-1 ) + ( 1 * 2-2 ) + ( 0 * 2-3 ) + ( 1 * 2-4 ) ) = 0.8125
3. 其他进制
- 八(十六)进制就是讲基数换成 8(16),方法不变
- 可以先将十进制(二进制)转换为二进制(十进制),再与八进制(十六进制)转换,使用划线方法
⚡⚡ 在 JavaScript 中使用 toString 进行进制转换
Number(0.1).toString(2);
// "0.0001100110011001100110011001100110011001100110011001101"
Number(0x1a).toString(8);
// 0o32
- 实际上 v8 引擎会自动转换进制
0o32 === 0x1a; // true
0x1a === 26; // true
三、 浮点数
1. 一般浮点数定义
-
浮点数:小数点可以浮动的数,表示为
N = M · R**E
- M 尾数,可正可负
- E 阶码,可正可负
- R 基数
以 R=2 为例,数 N 由于小数点的的浮动可以有不同的形式
N = 11.0101 = 0.110101 x 2**10 = 0.00110101 x 2**100 = 1.10101 x 2**1 = 1101.01 x 2**-10 ...
-
为提高精度以及便于比较,规定浮点数尾数用纯小数,上例中
0.110101 x 2**10
和0.00110101 x 2**100
符合- 尾数最高位为 1 的浮点数称为规格化数,
0.110101 x 2**10
就是规格化形式 - 浮点数表示成规格化后,精度最高
- 尾数最高位为 1 的浮点数称为规格化数,
2. IEEE754 标准中的双精度浮点数
a. double 双精度浮点数由 64 位固定长度表示,分为三部分
- 符号位 S:第 1 位是正、负数符号位(sign),0 代表正数,1 代表负数
- 指数位 E(阶码):用移码表示,中间 11 位存储指数(exponent),表示次方数
- 尾数位 M:用原码表示,最后的 52 位是尾数(mantissa),超出的部分自动1 进 0 舍
b. 尾数规格化形式
1.fff...fff
-
小数点其实是不存在的,只是为了区分
-
小数点左边的数(整数位)在标准中总是 1,规定中自动省略,称为隐藏位
// 原始十进制 13.125 // 二进制 1101.001 // 二进制浮点表示 1.101001 x 2**11 // IEEE754 双精度浮点表示 -> 符号位 0 -> 阶码偏移 000 0000 0011 + 011 1111 1111 = 100 0000 0010 -> 尾数规格化 // 最高位的1被隐藏,还原成普通二进制浮点数的时候要注意将1还原 1010 0100 0000 ... 0000(共52位)
c. IEEE754 中的指数偏移
IEEE754 规定了一个偏移量,指数部分每次都加上这个偏移量,这样即使指数为负数,加上偏移量保证为正数(正数方便比较大小)
-
double 双精度的指数部分为二进制的 11 位,表示的数据范围是
0~2047(2**11 - 1)
-
IEEE754 规定1023为双精度的偏移量
Bias
,所以指数 E 取值[0,1022]表示阶码为负,[1024,2047]表示阶码为正- 当指数不全是 0 也不全是 1 时(规格化的数值),指数 E 可以表示范围[1, 2046],阶码公式为
E-Bias
,所以实际取值范围[-1022, 1023]
- 当指数全为 0(非规格化数值),阶码公式为
1-Bias
,即1-1023 = -1022
- 当指数位全为 1 的时候(特殊值),可用来表示三个特殊值
- 尾数 M ≠ 0,表示
NaN
- 尾数 M = 0,符号位 S=0,表示正无穷
- 尾数 M = 0,符号位 S=1,表示负无穷
- 尾数 M ≠ 0,表示
- 当指数不全是 0 也不全是 1 时(规格化的数值),指数 E 可以表示范围[1, 2046],阶码公式为
按照 IEEE754 标准计算 78.735 的存储方式
- S = 0
- E = 1029(
1029 - 1023= 6
),转为二进制10000000101
- M = 001110011
0(sign) 10000000101(exponent) 0011 1001 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
d. 用移码表示阶码(指数)
-
阶码 = 移码 - 1
// 上述中78.735用双精度浮点数表示,指数是6 E = 6 + 1023 = 1029 // 1029 100000000101 // 6 00000000110[原] = 00000000110[补] = 100000000101[移] // 0.1用浮点数表示,指数是-4 E = -4 + 1023 = 1019 // 1019 01111111011 // -4 10000000100[原] = 11111111100[补] = 01111111011[移]
e. 计算公式
3. 双精度浮点数的加、减法运算
💡 先将 IEEE754 形式转为普通二进制数或者普通浮点表示形式进行加减运算
- IEEE754 有一个隐藏位,不能直接进行加减运算
- 转为普通浮点数形式,进行浮点数加减运算
a. 0 操作数的检查,如果判断两个操作数中有一个数是 0,则可以直接返回结果
b. 比较阶码大小并完成对阶
- 先将 IEEE754 表示的双精度浮点数转为一般浮点数形式
△E = Ex-Ey
- △E = 0,则表示对齐,尾数直接进行加减运算
- △E ≠ 0,则不对齐,则需要对齐操作
- 通过尾数的移动来改变阶码,达到对齐的效果
- 对阶操作规定使 尾数右移。因为尾数左移会造成最高有效位的丢失,误差较大;右移最低有效位会丢失,但是误差较小(右移丢失的位要判断舍入)
- 小阶向大阶看齐
- 阶码小的浮点数的尾数向右移动(相当于小数点左移)
- 尾数每右移一位,其阶码加 1
c. 尾数求和运算,按照“定点数加减规则”进行运算
- 用补码进行加减法运算
- 使用两位符号位的形式,最高位为实际符号
d. 结果规格化
-
当 R = 2,尾数规格化形式
1/2 ≤ |S| < 1
- 当 S > 0,尾数的补码规格化形式:[S]补 =
00.1xxx...x
- 当 S < 0,尾数的补码规格化形式:[S]补 =
11.0xxx...x
- 当尾数的最高数值位于符号位不同时,即为规格化形式
- 当 S > 0,尾数的补码规格化形式:[S]补 =
-
左规:当出现
00.0xxx...x
或者11.1xxx...x
时,尾数左移,阶码减小,直到符合规格化格式// 阶码和尾数都用两位符号位 // 用分号将符号位和数值位分开 [x+y]补 = 00,11; 11.1001 // 左规 [x+y]补 = 00,10; 11.0010 // x+y = (-0.1110) x 2**10
-
右规:当出现
01.xxx...x
或10.xxx...x
时,表示溢出(定点运算中不允许),浮点运算中可以将尾数向右移,阶码增加,直到符合规格化形式
e. 舍入和溢出处理
- 两种舍入方法
舍0进1
和恒置1
- 先将尾数规格化,再判断是否溢出
四、JavaScript 中的一道奇怪的加法(浮点数精度问题)
0.1 + 0.2 !== 0.3;
0.1 + 0.2;
// 0.30000000000000004
在 JS 中,(x + y) + z = x +(y + z)
不一定成立
0.1 + 0.2 + 0.3;
// 0.60000000000000001
0.1 + (0.2 + 0.3);
// 0.6
1. 计算机存储的数据都是二进制
- 在计算 0.1 + 0.2 的时候,计算机其实计算的是存储的二进制数
- 0.1 转为二进制是
0.000110011001100...
,(1100 循环) - 0.2 转为二进制是
0.001100110011001...
,(1100 循环)
2. JavaScript 如何存储数字
- JavaScript 内部,整数和浮点数都只有一种类型
Number
,采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值 - 遵循 IEEE754 标准,使用 64 位固定长度表示,即标准的
double双精度浮点数
- 数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第 54 位及后面的位就会被丢弃
- 归一化处理整数和小数,节省存储空间
3. 0.1 + 0.2
-
0.1
和0.2
在计算机中不可能按照无限循环方式存储,所以就要保留小数点后一定的位数,JS 采用 IEEE754 标准-
0.1
// 二进制表示 Number(0.1).toString(2) 0.000110011001100...(1100无限循环) // 规格化二进制浮点数 😏 0.11001100...(1100无限循环) x 2**-11 // IEEE754 双精度表示 -> 符号位 0 -> 阶码偏移 -4 + 1023 = 1019 = 01111111011 -> 规格化尾数 1001...1010 -> 0;1001...1010 x 2**01111111011 // 转成十进制数 0.100000000000000005551115123126 因此就出现了浮点误差
-
0.2
// 二进制表示 0.00110011001100...(1100无限循环) // 规格化二进制浮点数 😏 0.11001100...(1100无限循环) x 2**-10 // IEEE754 双精度表示 -> 符号位 0 -> 阶码偏移 -3 + 1023 = 1020 = 01111111100 -> 规格化尾数 1001...1010 -> 0;1001...1010 x 2**01111111100
-
-
在计算时,先将 IEEE754 表示的浮点数形式转为规格化的二进制浮点数 😏
-
两个数的阶码不相等,需要进行对阶
- 小阶向大阶对齐
- 0.1 的阶码小,0.1 的尾数右移一位,阶码增加 1,则阶码对齐
// 0.1对阶后尾数 01100110011001100110011001100110011001100110011001101; // 0.2尾数 11001100110011001100110011001100110011001100110011010;
-
尾数求和
- 和定点数的加减运算一样
- 采用两位符号位的补码运算,[A]补 + [B]补 = [A+B]补
- 0.1 和 0.2 都是正数,补码等于原码
00.01100110011001100110011001100110011001100110011001101 + 00.11001100110011001100110011001100110011001100110011010 = 01.00110011001100110011001100110011001100110011001101000 -> 结果右规 0.100110011001100110011001100110011001100110011001101 x 2**-1 -> 真值 +0.0100110011001100110011001100110011001100110011001101 -> 转为十进制 0.30000000000000004
1-0.9=0.09999999999999998
同样是浮点数精度问题
4. 为什么 JavaScript 可以表示 0.1
a. 0.1 转为浮点数储存后,再转为十进制数,由于精度问题,已经不再等于 0.1
0;1001...1010 x 2**01111111011
// 转成十进制数
0.100000000000000005551115123126
b. 在使用时依然显示的准确的 0.1
-
IEEE754 标准中的双精度浮点数的尾数固定长度是 52 位,还有 1 位隐藏位,一共有 53 位,最多可以表示的数
2**53 = 9007199254740992
-
9007199254740992
用科学计数法表示9.007199254740992 x 10**16
,16是 JS 最多能表示的精度长度 -
超过精度长度的数会做凑整处理,或使用
toPrecision(16)
做精度运算var n = 0.1; n.totoPrecision(16); // "0.1000000000000000" n.totoPrecision(25); // "0.1000000000000000055511151"
5. 如何表示 0.1 + 0.2 和 0.3 的关系
使用 JavaScript 提供的最小精度值 Number.EPSILON
:
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON;
JavaScript 认为运算结果的误差小于这个值,则认为没有误差
五、如何解决浮点误差
- 安利camsong 写的依赖包 github.com/dt-fe/numbe…
1. toPrecision
-
以指定的精度返回该数值对象的字符串表示
-
精度是从左到右第一个不为 0 的数起算,和小数点位置无关
-
被舍去的最高位会做四舍五入
(1.005).toPrecision(17); // "1.0049999999999999" (123.624).toPrecision(3); // "124"
1.005 的精度放大后为什么小于原来的值了?
- JavaScript 中所有的数只有一种
Number
类型,符合 IEEE754 标准,所以就有浮点数精度问题 - 由于 JS 能准确表示一个数的最大精度是 16,所以数值在表示时默认取 16 位精度,超出部分做取整
- 当放大精度后,原来位置的数值被逐渐还原,所以发现不在再准确等于这个值
2. toFixed
-
使用定点表示法来格式化一个数,也就是保留小数点后多少位
-
保留的位数一般选择 0~20,这个范围内不会出现
RangeError
。如果不加参数则默认使用 0 -
如果数值大于
1e+21
,该方法会简单的调用Number.prototype.toString()
,返回指数计数格式的字符串const num = 12345.6789; num.toFixed(); // "12346" num.toFixed(6); // "12346.678900" (1.23e10).toFixed(2); // "12300000000.00" (1.23e30).toFixed(); // "1.23e+30"
-
在做四舍五入时,有一个不可避免的 bug
(1.005).toFixed(2); // "1.00",而不是"1.01"
- 1.005 在实际显示时,是默认选择 16 为精度取整了的,而实际存储的是双精度浮点数,尾数精度要大于 16 位的
- 当使用
toFixed
(包括toPrecision
)时,实际上使用的是存储的双精度浮点数"1.00499999999999989341858963598497211933135986328125"
,进行定点数格式化 - 因为小数点后保留 2 位,所以要看第 3 位的取舍,由于第 3 位是
4
,根据四舍五入舍去,所以最终为1.00
3. 数据展示类的解决
-
建议使用
toPrecision
凑整并使用parseFloat
转成数字后显示function displayNum(num, precision = 12) { return +parseFloat(num, toPrecision(precision)); }
- 选择
12
的精度,是经验选择,该精度可以解决大部分0001
和0009
的问题
- 选择
4. 数据运算类
- 进行四则运算,不应通过
toPrecision
进行精度凑整,这样最终结果误差会很大 - 正确做法:将需要计算的数字升级(乘以 10 的 n 次幂)成计算机能够精确识别的整数,等计算完毕再降级(除以 10 的 n 次幂)
- 遇到科学计数法如
2.3e+1
(当数字精度大于 21 时,数字会强制转为科学计数法形式显示)时还需要特别处理一下 - 具体做法可以看这篇关于 javascript 中对浮点加,减,乘,除的精度分析
六、最大数值、安全整数和大数危机
1. 最大数值 Number.MAX_VALUE
和最小正数
-
符号位 0,阶码取 1023,尾数全为 1,可表示最大值,在 JavaScript 中,可以使用
Number
的静态属性Number.MAX_VALUE
表示const max = (-1) ** 0 * 2 ** 1023 * (Number.parseInt('1'.repeat(53), 2) * 2 ** -52); // 1.7976931348623157e+308 Number.MAX_VALUE; // 1.7976931348623157e+308
-
符号位为 0,阶码取 0(当指数全为 0,即非规格化数值,阶码公式为
1-Bias
,即1-1023 = -1022
),尾数仅最后一位为 1,表示最小正数const min = (-1) ** 0 * 2 ** -1022 * (Number.parseInt('0'.repeat(52) + '1', 2) * 2 ** -52); // 5e-324 Number.MIN_VALUE; // 5e-324
-
理论上最大值是
Math.pow(2, 1024) - 1
,但是Math.pow(2, 1024)
已经是Infinity
,所以不能用这样的表示法Infinity - 1; // Infinity Infinity - Number.MAX_VALUE; // Infinity Infinity - Infinity; // NaN
2. 最大安全整数Number.MAX_SAFE_INTEGER
和最小安全整数Number.MIN_SAFE_INTEGER
-
安全整数表示在这个范围内,所有的整数都有唯一的浮点数表示,范围之外的数由于精度问题无法准确表示
-
最大安全整数
Number.MAX_SAFE_INTEGER
是2**53 -1
,这个数字表明 JS 中 number 的最大精度不超过 16 位// 2**53 - 1 符号位:0,指数:52,尾数:111...1(一共52个1,不包含隐藏位) // 2**53 符号位:0,指数:53,尾数:000...00(一共53个0,不包含隐藏位) // 2**53 + 1 符号位:0,指数:53,尾数:000...01(一共52个0,第53位为1,不包含隐藏位)
- 由于 IEEE754 标准中尾数位数是 52 位,从第 53 位往后全都舍去
2**53
和2**53 + 1
尾数都有 53 位,按照标准会将第 53 位舍去,所以这些数是不能准确表示的(2**53, 2**54)
只能精确表示偶数(2**54, 2**55)
只能精确表示 4 的倍数,因为会损失第 53、54 位共两位精度- 以此类推
-
最小安全整数,就是最大安全整数的负值
3. Number.EPSILON
- 1 与大于 1 的最小浮点数之差
- JavaScript 能够表示的最小精度
- 认为运算结果的误差小于这个值,则认为没有误差
4. 如何处理大数危机
💡 目前大数问题一般是转为字符串,虽然保证了精度,但是运算效率就会降低
-
TC39 已经有第 3 阶段草案proposal bigint,但是目前需要使用 Babel7.0 实现
Chrome 69+ 的浏览器已经支持该类型,其他浏览器还未支持
-
生成任意精度的整数
-
草案中提到,ECMAScript 会有两个内置的数字类型:
Number
和Bigint
,所以Bigint
是全新的原始类型 -
使用 BigInt(number),将
Number
转为Bigint
,标志是以整数后以n
结尾BigInt(123); // 123n typeof 123n; // "bigint" typeof 123; // "number"
-
Bigint
严格不等于Number
,但是在用两个等号判断时,则有可能相等,并且0n
在if
条件语句中为假123n === 123; // false 123n == 123; // true 0n === false; // false 0n == false; // true
-
BigInt
顾名思义为大整数,所以如果用它来转换一个小数,会抛出异常;如果传入科学计数法的数值,则会将其展开1.2n; // Uncaught SyntaxError: Invalid or unexpected token BigInt(1.23); // Uncaught RangeError: The number 1.23 cannot be converted to a BigInt because it is not an integer BigInt(1.23e10); // 12300000000n
-
不允许
BigInt
和Number
混合运算,因为BigInt
不会发生隐式类型转换,必须显示调用来转换为其他类型1 + 1n; // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions BigInt(123) + 123n; // 246n
-
BigInt
可以和Number
进行比较,不过当数值部分相等时,判断会有奇怪的结果123 < 125n; // true 123 == 123n; // true 123 === 123n; // false 123 < 123n; // false 123 > 123n; // false 123 <= 123n; // true 123 >= 123n; // true
-
传递给
BigInt
超过 64 位整数范围的值(例如,63 位数值 + 1 位符号位),就会发生溢出
-
-
使用第三方库 bignumber.js
const BN = require('bignumber.js'); const big = new BN('8182931235402704882'); big; // -> { c: [81829, 31235402704882]; e: 18; s: 1; }
5. 面试题:两个大数求和
- 描述:对于两个超过了 JS 安全范围的整数,用加法求和并不能得到准确的结果,现在希望写一种方式能够对两个超过了 JS 安全范围的整数求和得到准确的结果
/**
* JavaScript安全范围数之外的数求和
* 思路:
* 1.将大数以字符串表示
* 2.将字符串转为数组,并反转数组,按照低位到高位(个、十、百、千...)
* 3.按位相加,设置进位标志
* 4.结果任以字符串输出
* @param {string} a
* @param {string} b
* @returns {string} 返回结果为字符串
*/
function bnSum(a, b) {
// 通过交换确保参数a的长度最大
if (a.length < b.length) {
[a, b] = [b, a];
}
// 转为数组
let [arr1, arr2] = [[...a].reverse(), [...b].reverse()];
// 进位标志
let num = 0;
for (let i = 0; i < arr1.length; i++) {
// 判断位数少的数在该位置上是否还有值
if (arr2[i]) {
arr1[i] = Number.parseInt(arr1[i]) + Number.parseInt(arr2[i]) + num;
} else {
arr1[i] = Number.parseInt(arr1[i]) + num;
}
// 判断该位置的和有没有进位
if (arr1[i] >= 10) {
[arr1[i], num] = [arr1[i] % 10, 1];
} else {
num = 0;
}
}
// 判断最高位有没有进位
if (num > 0) {
arr1[arr1.length] = num;
}
return arr1.reverse().join('');
}