You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The 53-bit significand precision gives from 15 to 17 significant decimal digits precision (2−53 ≈ 1.11 × 10−16). If a decimal string with at most 15 significant digits is converted to IEEE 754 double-precision representation, and then converted back to a decimal string with the same number of digits, the final result should match the original string. If an IEEE 754 double-precision number is converted to a decimal string with at least 17 significant digits, and then converted back to double-precision representation, the final result must match the original number.[1]
In Java, the bitwise operators work with integers. JavaScript doesn't have integers. It only has double precision floating-point numbers. So, the bitwise operators convert their number operands into integers, do their business, and then convert them back. In most languages, these operators are very close to the hardware and very fast. In JavaScript, they are very far from the hardware and very slow. JavaScript is rarely used for doing bit manipulation.
-- Douglas Crockford in "JavaScript: The Good Parts", Appendix B, Bitwise Operators (emphasis added)
最近看到几篇关于讲解 JS 中 number 的深度好文,文中解析了 js number 的一些常见问题,正好之前自己对这块的内容掌握不深,所以今天就刚好扩展整理一下 JS 中 number 相关的内容,跟大家分享下
诡异的 JS
首先先来看几个 JS 中不太符合常理的诡异现象
原因所在
如果上面的现象你能够完全以自己的方式理解,那么证明你对 JS 中的 number 特性已经十分了解,就不需要看后面的内容了
如果仍有疑问,可以继续往后看具体的原因
Number 定义
在 JS 中没有真正意义上的整数,仅拥有一个
number
类型,不像 C 拥有int、float、double
这几种类型,这在一定程度上降低了 JS 语言的上手成本,但是也带来了一些理解上的误差JS number 中的整数就是没有小数的十进制数,所以第一个例子也就说得通了
Number 实现
在 IEEE 754 中定义了 64 位双精度浮点数的标准,也就是 C 语言中的
double
类型,JS 是按照此规准来实现的number
这里的具体转换过程我们后面详细演示,这里先铺垫介绍下 64 位表示法的设计方案,它由符号位,指数位和尾数位组成(这里也看出了前人对如何使用最小的空间表示最多的数字范围的权衡思考,如果让大家来设计,不知道有没有更好的思想呢):
符号 sign
:指数 exponent
:尾数 mantissa
:Number 运算
接下来我们详细看看,JS(或者大多数编程语言)对于 number 数值的运算转换流程,相信分析过后,对于前面列举的很多诡异现象都能够有更好的理解
运算流程主要有以下几步:
1. 十进制转二进制
这里以十进制为例,把带小数的十进制数
106.6953125
转换成二进制,来看看详细的流程这里转换后获得的二进制是:
1101010.1011001
2. 二进制转科学计数法
1101010.1011001
转为1.1010101011001 * 2^6
,这里进行转换的目的也是为了使用尽可能少的数字来表示对应的 number 值这里举得带小数的例子尾数比较长,可能看不出来压缩位数的效果。假设需要转换的是
1000000
这个二进制值,那么科学计数法就变成了1 * 2^6
,只需要1 和 6(二进制 110)
,我们就能知道原数为10000
了那么根据前文的定义,我们可以知道,这里的指数值为 6
3. 转换成 JS 支持的存储格式【重点】
根据前文对于双精度类型格式的介绍,我们在这个例子上对其展开说明下其定义的内容:
符号 sign
:指数 exponent
:Infinity
(需要看符号)NaN
小数
时,指数为负数
,如 0.1,即 1 * 2^-1^较大的数
时,指数为正数
,如 100,即 1 * 2^3^尾数 mantissa
:1.xxx
这样的格式,所以为了空间尽可能的紧凑,我们将第一个 1 舍去,仅存储小数点后的尾数综上,可以归纳为这样的数学转换表达式【数学之美了】
根据上面的规则,我们分析
1.1010101011001 * 2^6
可以得出最终转换结果如图
4. 进行运算
我们上面举的例子,大家可以发现,最后的尾数位是没有无限循环的,这就证明了这个数据能够比较好的被二进制表达,没有误差,所以误差问题没有暴露出来
这里我们就以前文中的几个 demo 为例,看看运算过程
demo 1
我们仔细看看这个例子不难发现,都是乘以 100,结果一个比应有结果小,一个比应有结果大,我们来看看原因是啥
首先
18.9
这个十进制数,我们按照前文的方法来计算一遍,结果如下不难发现,尾数部分是无限循环的,舍弃后面的数字必然导致转换后的值比现在小,再通过乘法放大,结果也就说的通了
那
64.68
经过计算后为啥还大了呢,这里看看二进制的转换结果发现依然有数据丢失,但是根据 JS 引擎定义的规则,最后丢失的值会进行 0 舍 1 入
64.68 转为二进制后是
1000000.10101110000101000111101011100001010001111011
,但实际上 64.68 尾数部分的二进制真实的是10101110000101000111
(无限循环),受存储空间限制,存储策略使最后四位从原本的1010
变为1011
,所以存储在 JavaScript 中的数比数学意义上的 64.68 会大所以由于精度问题,结果可能会比预期中的大或者小,都是合理的
demo 2
这个问题实在太经典了,我们来看看
由于 0.1、0.2、0.3 都无法准确地用二进制表示,所以都存在一定的误差:
121010121010120011通过计算之后,我们页可以发现结果确实并不相同,所以两者不相等是合理的
120011120100但是,真的就是这么简单吗?
我们来看看精度相关的这个例子,不知道大家有没有发现端倪
这里我们发现
在十进制下,0.1 + 0.2 的第 17 位精度值结尾有 4,0.1 的结尾也有 1,那么既然 0.1+0.2 !== 0.3,那么为什么 0.1 === 0.1 呢?
这里就要分析下,双精度浮点数是按照什么规则来截断的了
维基百科中这段话解释了规则,简单来说,就是如果当 17 位有效数字的十进制数字字符串转回双精度浮点数时,和之前的相同,那么取最短的十进制数即可
这里
0.1
和0.10000000000000001
转成双精度浮点数的存储是一样的,所以取0.1
即可但是
0.1 + 0.2
不满足,所以不等于0.3
demo 3
按照常理来说,应该是四舍五入,获取 1.01 的结果才对,这里为什么是 1.00 呢,我们还是得看下二进制表示结果
转换为十进制之后的结果是
1.00499999999999989341858963598
,当这个时候我们再次调用 toFixed(2),小数点后第二位为零,第三位为 4,所以被舍弃掉了,这也是一个由精度影响的结果扩展知识
怎么解决上面发现的误差问题
核心思想主要是 对有效数字进行控制 或者 将数字转换成字符串进行运算
JS 安全的 number 的数值范围
这里的安全,指的是双精度数和十进制数能够一一对应。超过这个范围,会有两个或更多整数的双精度表示是相同的;反过来说,超过这个范围,有的整数是无法精确表示的,只能 round 到与它相近的浮点数(说到底就是科学计数法)表示,这种情况下叫做不安全整数
在 JS 中,官方给出的安全值范围如下:
number.MAX_SAFE_INTEGER
=2 ** 53 - 1
number.MIN_SAFE_INTEGER
=-1 * (2 ** 53 - 1)
那么实际范围就是 [2 ** 53 - 1, -1 * (2 ** 53 - 1)]
那么为啥是 53 呢,因为二进制表示中,有效数字最长为53个二进制位( 52 位尾数 + 有效数字第一位的 1[被舍弃的 1] )
举个例子:
但是对于 2^53^ + 1 的存法就出现了问题:符号位:0,指数:53,尾数:1.00000...000(一共52个0),这个值的结果和 2^53^ 一样
所以这也说明了为什么安全范围不包括边界,同时也解释了为什么 demo 4 会无限循环了
demo5 中的
180143985094813214124
=>180143985094813220000
也是因为数据超出安全范围,导致双精度结果和其他数字重叠,导致结果异常原数二进制表示
转换后的异常数的二进制表示
一模一样,神仙都分不出来
大数危机
从前面的讲解中,我们不难发现,如果二进制的结果一样,那么根本无法用十进制的数完全无法与二进制一对一的表示
我们在实际可支持的数字表示范围中,安全的仅仅占很小一部分,所以称为大数危机
上图中的双精度范围为(-2^1024^ ~ 2^1024^)[下方的 Floating-Point Numbers]
我们使用的 (-2^53^ ~ 2^53^)只占真实 number 中的很小一部分
位运算
从上面的引用中也能发现,JS 中的位运算其实是做了一层包装的:并不是简单的位运算,而是做了一层转换在其中
除此之外还有一些补充的位运算规则:
1. 值相等但判等失败
这里看上去一个正整数,进行按位或,理论上没啥问题对吧,但是按位或的结果却并不是原始值
这里的原因主要是位运算仅支持 32 位导致的
原来的
2 ** 31
的二进制表示如下10000000000000000000000000000000
,进行按位或时,1 当成了符号位,自然结果会是 0 或者 负的 2^31^2.位运算后正数变负数
这里的原因依然是仅支持 32 位运算导致的,二进制原数为
01111111111111111111111111111111
,左移一位后,变成了11111111111111111111111111111110
,导致变成了 -2同理,原数为
1111111111111111111111111111111
(31位,开头的 0 省略了),右移后变成11111111111111111111111111111111
(32位,开头补充了个 1),导致结果变成 -1ES6 语法中的 BigInt
在新的 ES6 语法中,JS 新增了 BigInt 类型的变量来支持安全范围外的数字
可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()
个人理解是官方将上述的第三方库以 polyfill 的方式注入到了 BigInt 中,进行了整合而提供的,本质上应该没有区别
结语
这篇文章参考了很大多大佬的回答和文章,综合了目前可能会遇到的 number 相关的问题和原理,希望能够帮助大家理解和掌握这块的内容
感慨一下,计算机的世界真的是充满数学之美
参考文档
The text was updated successfully, but these errors were encountered: