js小数精度
0.1+0.2不等于0.3?
https://www.javascriptc.com/books/nodejs-roadmap/javascript/floating-point-number-0.1-0.2.html
https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/computercode.html
https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html
存储方式
JavaScript使用Number类型表示数字(整数和浮点数),遵循 IEEE 754 标准通过双精度64位(8字节)来表示一个数字
64位 = 1位符号 + 11位指数 + 52位数字

- 符号位: 0表示正数,1表示负数
- 指数位: 科学计数法, e的n次方带上偏移, 实际指数位 = 指数位的的值 + 1023, 再转成 11 位二进制
- 尾数位: 1.xx…xx, 第一位是1, 默认不写, 后面的xx有52位, 其中第 53 及之后的位会决定是否进 1
- 第 53 位是 0, 后面的全部舍去
- 第 53 位是 1
- 如果第 54 位及后面任意一位是 1, 向上进 1
- 如果第 54 位是 0
- 如果 52 位是 0, 则舍去
- 如果 52 位是 1, 则进 1
十/二进制转换
整数
B = kn * 2^n + … + k3 * 2^3 + k2 * 2^2 + k1 * 2^1 + k0 * 2^0
除2取余, 直到商为0, 倒序写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 以10为例
10/2 = 5 ... 0
5/2 = 2 ... 1
2/2 = 1 ... 0
1/2 = 0 ... 1
10 = 2^3 + 2^1
10 = 1010
符号位 正数, 为0
偏移后的指数位 用科学计数法表示, 1.01*10^3, 相当于小数点左移3位, 还需要带上偏移, 偏移量为 2^(11-1)-1=1023, 1023+3=1026=2^10+2^1, 二进制表示为 10 000 000 010
(偏移量: 2^11 可以表示2048种状态, 可以表示[-1023 ~ 1024] 范围的指数, 使 指数 + 偏移量 = 一个非负整数, 就不用担心如何表示负数)
尾数位 1.01 隐藏高位1和小数点, 为 01, 后面补0到52位
在内存中存储:
0 10 000 000 010 0100000000000000000000000000000000000000000000000000
|
小数部分
B = k1 * 2^-1 + k2 * 2^-2 + … + kn * 2^-n
小数: 乘2取整, 直到不存在小数为止, 正序写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| 以0.1为例
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.4 * 2 = 0.8 取 0
0.8 * 2 = 1.6 取 1
0.6 * 2 = 1.2 取 1
...
0.1 = 0.0 0011 0011 (0011无限循环)
符号位: 0
指数位: 1.1 0011(循环) * 2^-4, -4+1023=1019=1111111011(二进制)
尾数位: 1 0011 0011 (有循环, 截取到52位, 第53位进1舍0,精度丢失)
在内存中存储:
0 01111111011 1001100110011001100110011001100110011001100110011010
|

浮点数运算
对阶
浮点数加减首先要判断两数的指数位是否相同(小数点位置是否对齐),若两数指数位不同,需要对阶保证指数位相同。
对阶时遵守小阶向大阶看齐原则,尾数向右移位,每移动一位,指数位加 1 直到指数位相同,即完成对阶。
尾数右移 1 位之后最高位空出来, 补全方法:
- 逻辑右移:最高位永远补 0
- 算术右移:不改变最高位值,是 1 补 1,是 0 补 0
1
2
3
4
5
6
7
8
9
10
11
12
| S E M
0 01111111011 1001100110011001100110011001100110011001100110011010 // 0.1
0 01111111100 1001100110011001100110011001100110011001100110011010 // 0.2
// 0.1 移动之前
0 01111111011 1001100110011001100110011001100110011001100110011010
// 0.1 右移 1 位之后尾数最高位空出一位,(0 舍 1 入,此处舍去末尾 0)
0 01111111100 100110011001100110011001100110011001100110011001101(0)
// 0.1 前面还有隐藏掉最高位1, 右移 1 位完成
0 01111111100 1100110011001100110011001100110011001100110011001101
|
尾数求和
1
2
3
| 0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
10.0110011001100110011001100110011001100110011001100111
|
规格化和舍入
产生进位,阶码需要 + 1
1
2
3
| S E
0 01111111100 + 1
= 0 01111111101
|
尾部进位 2 位,去除最高位默认的 1, 最低位进行舍入操作: 最低有效位上加 1,若为 0 则直接舍去,若为 1 继续加 1
1
2
3
4
| 100110011001100110011001100110011001100110011001100111 // + 1
= 00110011001100110011001100110011001100110011001101000 // 去除最高位默认的 1
= 00110011001100110011001100110011001100110011001101000 // 最后一位 0 舍去
= 0011001100110011001100110011001100110011001100110100 // 尾数最后结果
|
最终存储
1
| 0 01111111101 0011001100110011001100110011001100110011001100110100
|
转成十进制
原码, 反码, 补码
- 一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1
比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011
1
2
| 0000 0001的真值 = +000 0001 = +1
1000 0001的真值 = –000 0001 = –1
|
1
| [+1] = [00000001]原 = [00000001]反 = [00000001]补
|
- 负数的符号位一值不变, 反码 = 原码除符号位全部取反, 补码=符号位不变反码+1
1
| [-1] = [10000001]原 = [11111110]反 = [11111111]补
|
- 为什么要有反码? 解决原码做减法的问题
- 为什么要有补码? 解决了0的符号以及两个编码的问题
js number安全范围
1
2
| Number.MAX_SAFE_INTEGER = 2^53 - 1 (52个1加上省略的, 共53个1)
Number.MIN_SAFE_INTEGER = -(2^53 - 1)
|
Number.MAX_SAFE_INTEGER
的计算:
- 符号位: 正数为 0
- 尾数位: 1.111… 111 (52 个 1 加上小数点前面隐藏的 1, 共 53)
- 指数位: 52 (小数点左移 52 位) + 1023 = 1075
- 对应的二进制为
1
| 0 100 0011 10011 111...111 (52个1)
|
总结
二制制转换, 对阶, 舍入 过程中都可能导致精度丢失
如何解决
- 将数字转成整数
- 用第三方库
- Math.js
- big.js
- decimal. js