开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天
数字类型在JS中是如何存储的
JavaScript 不是类型语言。与许多其他编程语言不同,JavaScript 不定义不同类型的数字,比如整数、短、长、浮点等等。基于JavaScript的这种特点,在JavaScript中,数字不分为整数类型和浮点型类型,所有的数字都是由 浮点型类型。如下:
1.0 === 1 // true
JavaScript 采用 IEEE754 标准定义的 64 位浮点格式表示数字。此格式用 64 位存储数值,其中 0 到 51 存储数字(片段),52 到 62 存储指数,63 位存储符号。经典图如下:
- 第一部分(蓝色):用来存储符号位(sign),第1位:符号位,
0
表示正数,1
表示负数 - 第二部分(绿色):用来存储指数(exponent),第2位到第12位(共11位):指数部分
- 第三部分(红色):用来存储小数(fraction),第13位到第64位(共52位):小数部分(即有效数字)
这样的存储结构优点是可以归一化处理整数和小数,节省存储空间
经典问题: 0.1 + 0.2 不等于 0.3
原因:因为存储时有位数限制(64位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0舍1入),当再转换为十进制时就造成了计算误差
如何理解上面的话呢?举个🌰
比如一个数 1÷3=0.33333333......
3会一直无限循环,数学可以表示,但是计算机要存储,方便下次取出来再使用,但0.333333...... 这个数无限循环,再大的内存它也存不下,所以不能存储一个相对于数学来说的值,只能存储一个近似值。
回到上面的问题
在javascript
语言中,0.1 和 0.2 都转化成二进制后再进行运算,得到的值再转成十进制就和我们想象的不一样了
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004
所以0.1 + 0.2 === 0.3
输出的是false
解决方案
理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果
当你拿到 1.4000000000000001
这样的数据要展示时,建议使用 toPrecision
凑整并 parseFloat
转成数字后再显示,如下:
parseFloat(1.4000000000000001.toPrecision(12)) === 1.4 // True
封装成方法就是:
function strip(num, precision = 12) {
return +parseFloat(num.toPrecision(precision));
}
对于运算类操作,如 +-*/
,就不能使用 toPrecision
了。正确的做法是把小数转成整数后再运算。以加法为例:
/**
* 精确加法
*/
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
最后还可以使用第三方库,如Math.js
、BigDecimal.js
什么叫安全整数,最大安全整数是怎么得到的呢?
MDN上安全整数的描述
一个安全整数是一个符合下面条件的整数:
- 可以准确地表示为一个 IEEE-754 双精度数字,
- 其 IEEE-754 表示不能是舍入任何其他整数以适应 IEEE-754 表示的结果。.
可以使用 Number.isSafeInteger()
来判断一个数值是否为安全整数
那为什么安全整数的范围是-(2^53 - 1)
到 2^53 - 1
之间的数值(包含边界值)?
我们还是回到数字类型在JS中是如何存储的图上,表示整数的位置只有52位,最大值转成二进制就是52个1,52个1表示的就是2^53 - 1
,相对应的最小值就是-(2^53 - 1)
为什么不是指数部分决定的
0.1123 * 2^1024
当然这里面有人会问为什么不是指数部分决定呢。上面这个数的范围是不是比我们的讨论的数据范围更大呢。
其实并不是这样,因为实用指数表示并不能表示连续的数字。所以这个方案不可取。
那如果超过了安全整数了有什么问题?
看一下下面的例子:
Math.pow(2,53) - 1 === Number.MAX_SAFE_INTEGER // true
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true
Number.MAX_SAFE_INTEGER + 2 === Number.MAX_SAFE_INTEGER + 3 // false
很明显一旦超过了安全整数,就会发生精度缺失的问题。实际上存储后的整数如下:
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MAX_SAFE_INTEGER + 1 // 9007199254740992
Number.MAX_SAFE_INTEGER + 2 // 9007199254740992
Number.MAX_SAFE_INTEGER + 3 // 9007199254740994
Number.MAX_SAFE_INTEGER + 4 // 9007199254740996
也就是说在(-2^53, 2^53)范围内,双精度数表示和整数是一对一的,反过来说,在这个范围以内,所有的整数都有唯一的浮点数表示,这叫做安全整数。而超过这个范围,会有两个或更多整数的双精度表示是相同的;反过来说,超过这个范围,有的整数是无法精确表示的,只能round到与它相近的浮点数表示,这种情况下叫做不安全整数。
科学计数法
对于一个整数,可以很轻易转化成十进制或者二进制。但是对于一个浮点数来说,因为小数点的存在,小数点的位置不是固定的。解决思路就是使用科学计数法,这样小数点位置就固定了
而计算机只能用二进制(0或1)表示,二进制转换为科学记数法的公式如下:
X = a * 2^e
其中,a
的值为0或者1,e为小数点移动的位置
举个例子:
27.0转化成二进制为11011.0 ,科学计数法表示为:
1.10110 * 2 ^ 4
再来一个问题,那么为什么x=0.1
得到0.1
?
主要是存储二进制时小数点的偏移量最大为52位,最多可以表达的位数是2^53=9007199254740992
,对应科学计数尾数是 9.007199254740992
,这也是 JS 最多能表示的精度
它的长度是 16,所以可以使用 toPrecision(16)
来做精度运算,超过的精度会自动做凑整处理
.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1
但看到的 0.1
实际上并不是 0.1
。不信你可用更高的精度试试:
0.1.toPrecision(21) = 0.100000000000000005551
实战推演
符号位: 0表示正数,1表示负数
指数位: 因为e可以为正,可以为负数。比如 1.10110∗24 这个e为正数,如果是0.101那么用指数表示就是 1.01∗2−1 ,那么e为-1。同时要求先把e+指数偏移量,得到的结果再化成二进制,就是我们的指数位
小数部分(也称为阶数): 二进制下转换为科学记数法后小数点后面的数字。如 1.1011^4 的小数位为1011,它的总位数应为52位, 位数不够就用0补齐 因此也可以这样理解小数点的偏移量最大为52位,取点符号位和指数 那么这个52位就表示最大整数位为52位 即JS 中能精准表示的最大整数是 Math.pow(2, 53) 十进制即 90071992547409921
指数偏移量: 我们先看看指数部分,指数一共是 11 位,如果全部为 1,则最大能够表示 2的11次方−1=2047
。所以指数的范围是 [0, 2047]
。但是指数部分有负数,所以定义了一个偏移量,在 64 位浮点数中,偏移量为 1023( 2e−12^e - 12e−1,eee 为 111111)。减去偏移量之后,指数的范围变成了 [-1023, 1024] 。
但是指数全为 1 和全为 0 有特殊作用,所以我们可用的指数少了 -1023(对应指数全 0)和 1024(对应指数全 1),范围变成了 [-1022, 1023]。
计算机存储二进制即为:符号位+指数位+小数部分 (阶数) 27.5 转换为二进制11011.1
11011.1转换为科学记数法 1.10111∗24
符号位为0(正数)
指数位为4+指数偏移量1023 即1027 因为它是十进制的需要转换为二进制即 10000000011
小数部分为10111,补够52位即: 1011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
指数偏移量为 211−1 =1023
所以27.5存储为计算机的二进制标准形式为
符号位+指数位+小数部分 (阶数)
0+10000000011+011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
即:
0100 0000 0011 1011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
正好64位
注意区分:十进制转换为二进制数只保留52位有效数字(科学记数法),而计算机存储双精度浮点数为64位
再捋一下
计算机存储一个27.5的数字
- 首先把这个数字转换为二进制 11011.1
- 再把二进制转换为科学记数法 1.10111∗24
- 又因js存储数字用的是双精度浮点数【最多存储64位】 即 符号位【1】+指数位【4+1023(固定偏移量)=> 10000000011】+小数部分【10111(52位不够用0补齐)】
- 即 0100 0000 0011 1011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
那小数点前面的整数位不用存储吗?
不用 因为转化为二进制之后首位数都是1 ,计算机会自动处理
52位为什么可以表示53位小数
因为小数部分只需要表示尾数就可以,整数部分可定等于一
52位太多不好理解,假设我们以3位(bit)数
0.10 (二进制) 可以表示为 1.00 * 2^-1
0.01 (二进制) 可以表示为 1.00 * 2^-2
这样的话由于整数部分一定等于1,所以可以把整数部分省略。
也就是说3位数可以表示做小数表示的时候可以表示4位小数