深入JavaScript中的Number类型

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_44196299/article/details/101002952

在学习《JavaScript高级程序设计》的时候我们都会注意到书中特意强调的在JavaScript中0.1+0.2并不等于0.3这样一个浮点数计算错误的问题,以及大家都知道的JavaScript中最大整数范围到底是怎么产生的?因此查询了一些资料,对于JavaScript中的Number类型进行一个更为深入的了解,现整理如下:

简介

在JavaScript中数值只有一种,即Number类型,内部表示为双精度浮点型,即其他语言中的double类型,所以在JavaScript中实际上是没有整数类型的,数值都是按浮点数来处理的,存储方法相同,遵循IEEE 754国际标准,因此在JavaScript中3和3.0被视为同一个值,示例:

3.0 === 3 // true

对于整数情况,能够准确计算的整数范围为在-253~253之间,不包含两个端点,因此只要在这个范围内整数可以放心使用。除了十进制以外整数还可以通过八进制或十六进制的字面值来表示,其中八进制字面值的第一位必须是零,然后是八进制数字序列(0 ~ 7),如果字面值中的数值超出范围,那么前导零将被忽略,后面的数值被当作十进制解析,这儿需要注意在严格模式中八进制的这种表示会报错,ES6中进一步明确,八进制的表示要使用前缀0o,示例:

// 非严格模式
(function(){
	console.log(0o11 === 011)
}) // true

// 严格模式
(function(){
	'use strict';
	console.log(0o11 === 011)
})
// Uncaught GyntaxError

十六进制字面值前两位必须是0x,后跟任何十六进制数字(0 ~ 9以及A ~ F),其中A ~ F可以大写,也可以小写。ES6中又扩展了二进制的写法,使用前缀0b(或0B)。
虽然在JavaScript中无论是小数还是整数都是按照64位的浮点数形式存储,但是进行整数运算会自动转换为32位的有符号整数,例如位运算,有符号整数使用31位表示整数的数值,用第32位表示整数的符号,数值范围是-231 ~ 231

一、浮点型数值的保存

JavaScript中的Number类型使用的是双精度浮点型,即其他语言中的double类型,双精度浮点数使用8个字节即64bit来进行存储,现代计算机中浮点数大多是以国际标准IEEE 754来存储,存储过程分两步,
1、把浮点数转换为对应的二进制数,并用科学计数法表示
2、将转换之后的数通过IEEE 754标准表示成真正会在计算机存储的值。
根据IEEE 754标准任何一个二进制浮点数V都可以表示成:
V = ( 1 ) S M ( 2 ) E {V }= (-1)^{S}\cdot{M}\cdot(2)^{E}\\

  • (-1)S表示符号位,当S=0,V为正数,当S=1,V为负数
  • M表示有效数字,大于等于1,小于2
  • 2E表示指数位

举例:十进制的5.0,写成二进制是101.0,相当于1.01x22,其中S=0,M=1.01,E=2。

IEEE 754规定对于32位浮点数最高1位是符号位S,接下来8位是指数E,剩下的23位为有效数字M,具体如下图所示:
在这里插入图片描述
对于64位的浮点数最高1位是符号位S,接下来11位是指数E,剩下的52位是有效数字M,具体如下图所示:
在这里插入图片描述
注意:IEEE754 对于有效数字M和指数E还有一些特别的规定。
前面说过,1 <= M < 2,也就是说M总是可以写成1.xxxxxxx的形式,其中xxxxxxx表示效数部分。IEEE 754规定,在计算机内部保存M时默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxxx部分。比如保存1.01的时候只保存01,等到读取的时候再把第一位的1加上去,这样做的目的就是为了节省一位有效数字,以32位浮点数为例,留给有效数字M的只有23位,将第一位的1舍去以后等于保存24位有效数字。
至于指数E,情况较为复杂。
首先,E为一个无符号指数,这意味着,如果E为8位,它的取值范围为0 ~ 255,如果E为11位,它的取值范围为0 ~ 2047。但是我们知道科学计数中的E是是可以出现负值的,所以IEEE 754规定,E的真实值必须再减去一个中间数,对于8位的E,这个中间数是127,对于11位的E,这个中间数是1023。
比如210的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
然后指数E还可以分成三种情况:

  • E不全为0或不全为1:这时,浮点数就采用上面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
  • E全为0:这时,浮点数的指数E等于1 ~ 127(或1 ~ 1023),有效数字M不再加上第一位的1,而是还原成0.xxxxxxx的小数,这样做是为了表示±0,以及接近0的很小的数字。
  • E全为1:这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位S);如果有效数字M不全为0,表示这个数不是一个数(NaN)。

示例:浮点数9.0如何用二进制表示?还原成十进制又是多少?

首先,浮点数9.0等于二进制的1001.0,即1.001x23
那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010。
所以,写成二进制形式,应该是s+E+M,即0 10000010 001 0000 0000 0000 0000 0000。这个32位的二进制数,还原成十进制,正是1091567616。

二、数值范围

ECMAScript能够表示的数字的绝对值范围是5e-324 ~ 1.7976931348623157e+308,这两个取值可以通过Number.MIN_VALUE和Number.MAX_VALUE这两个字段来表示,如果某次计算的结果得到了一个超出JavaScript数值范围的,那么这个数值会自动被转换为特殊的Infinity值,具体来说,如果这个数是负数,则会被转换成-Infinity(负无穷),如果这个数值是正数,则会被转换成Infinity(正无穷)。
示例:

console.log(Number.MAX_VALUE) // 1.7976931348623157e+308
console.log(Number.MIN_VALUE) // 5e-324
console.log(Number.MAX_VALUE + Number.MAX_VALUE) // Infinity

那么这个取值范围是如何得到的呢?
前面说到JavaScript中数值的保存采用的是双精度浮点型,遵循IEEE 754标准,在ECMAScript规范中规定指数E的范围在-1074 ~ 971,双精度浮点型中有效数字M的存储位为52,但是有效数字M由于可以省略第一位1,节省一个存储位,因此有效数字M可以存储的范围为1 ~ 253,因此JavaScript中Number能表示的最大数字绝对值范围是 2-1074 ~ 2(53+971)
注:通过Number.isFinite()(ES6引入)和isFinite()方法可以判断一个数值是不是有穷的,即如果参数位于最小与最大数值之间时会返回true

三、精度丢失

在本文刚开始的时候我们提到在JavaScript中0.1+0.2不等于0.3,实际上所有浮点数值存储遵循IEEE 754标准的编程语言中都会存在这个问题,这是因为计算机中小数的存储先是转换成二进制进行存储的,而0.1、0.2转换成二进制分别为:

(0.1)10 => (00011001100110011001(1001)…)2
(0.2)10 => (00110011001100110011(0011)…)2

可以发现,0.1和0.2转成二进制之后都是一个无限循环的数,前面提到尾数位只能存储最多53位有效数字,这时候就必须来进行四舍五入了,而这个取舍的规则就是在IEEE 754中定义的,0.1最终能被存储的有效数字是

0001(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)101
+
(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)01
=
0100(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)111

最终的这个二进制数转换成十进制的就是0.30000000000000004,这儿需要注意,53位的存储位指的是能存53位有效数字,因此前置的0不算,要往后再取到53位有效数字为止。
因此精度丢失的问题实际上用一句话概括就是计算机中用二进制存储小数,而大部分小数转成二进制后都是无限循环的值,因此存在取舍问题,也就是精度丢失。
ES6在·Number对象上新增了一个极小常量:Number.EPSILON,值为2.220446049250313e-16,引入这么一个常量就是为了为浮点数计算设置一个误差范围,如果这个误差小于Number.EPSILON我们就认为得到了准确结果。

四、 最大安全整数

JavaScript中最大安全整数的范围是253 ~ 253,不包括两个端点,即-9007199254740991 ~ 9007199254740991,可以通过Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER字段查询,超出这个范围的整数计算都是不准确的,例如:

console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) // -9007199254740991
console.log(9007199254740991 + 2) // 9007199254740992

最大安全整数9007199254740991对应的二进制数如图:
在这里插入图片描述
53位有效数字都存储满了之后,想要表示更大的数字,就只能往指数数加一位,这时候尾数因为没有多余的存储空间,因此只能补0。
在这里插入图片描述
如图所示,在指数位为53的情况下,最后一位尾数位为0的数字可以被精确表示,而最后一位尾数为为1的数字都不能被精确表示。也就是可以被精确表示和不能被精确表示的比例是1:1。
同理,当指数为54的时候,只有最后两位尾数为00的可以被精确表示,也就是可以被精确表示和不能被精确表示的比例是1:3,当有效位数达到x(x>53)的时候,可以被精确表示和不能被精确表示的比例将是1 : 2^(x-53) - 1。
可以预见的是,在指数越来越高的时候,这个指数会成指数增长,因此在Number.MAX_SAFE_INTEGER ~ Number.MAX_VALUE之间可以被精确表示的整数可以说是凤毛麟角。

之所以会有最大安全整数这个概念,本质上还是因为数字类型在计算机中的存储结构。在尾数位不够补零之后,只要是多余的尾数为1所对应的整数都不能被精确表示。

可以发现,不管是浮点数计算的计算结果错误和大整数的计算结果错误,最终都可以归结到JS的精度只有53位(尾数只能存储53位的有效数字)。那么我们在日常工作中碰到这两个问题该如何解决呢?
大而全的解决方案就是使用mathjs,看一下mathjs的输出:

math.config({
    number: 'BigNumber',      
    precision: 64 
});

console.log(math.format(math.eval('0.1 + 0.2'))); // '0.3'

console.log(math.format(math.eval('0.23 * 0.34 * 0.92'))); // '0.071944'

console.log(math.format(math.eval('9007199254740991 + 2'))); 
// '9.007199254740993e+15'

参考资料:
从0.1+0.2=0.30000000000000004再看JS中的Number类型
浮点数的二进制表示
《ES6标准入门》 阮一峰

猜你喜欢

转载自blog.csdn.net/weixin_44196299/article/details/101002952