数字在JavaScript中是如何编译的

时间: 2018-05-16阅读: 251标签: js知识作者: _小生_

JavaScript中的所有数字都是浮点数。这篇博客文章解释了这些浮点数如何在64位二进制内部表示。由于特别考虑,本文中的数字将用整数表示,以便在阅读本文后,您将了解在以下交互中会发生什么:

(译者注:浮点数并不一定等于小数,定点数也并不一定就是整数。所谓浮点数就是小数点在逻辑上是不固定的,而定点数只能表示小数点固定的数值,具用浮点数或定点数表示某哪一种数要看用户赋予了这个数的意义是什么。)

    > 9007199254740992 + 1
    9007199254740992

    > 9007199254740992 + 2
    9007199254740994


JavaScript数字

JavaScript数字都是浮点数,按照IEEE 754 standard标准进行存储。该标准有几种格式。 JavaScript使用binary64或双精度。正如前面的名称所表示的,数字以二进制格式存储在64位中。这些比特分配如下:分数占据比特0到51,指数占据比特52到62,符号占用比特63。

sign (1 bit) 63

exponent (11 bit)

62

52

fraction (52 bit)

51

0

|

这些组件的工作原理如下:如果符号位为0,则数字为正数,否则为负数。粗略地说,分数包含数字的值,而指数表示该点的位置。在下面,我们经常使用二进制数字,这在浮点数方面有点不寻常。二进制数字将以前缀​​百分比符号(%)标记。虽然JavaScript数字以二进制格式存储,但默认输出为十进制[1]。在示例中,我们通常会使用该默认值。


分数

以下是表示非负浮点数的一种方法:有效数(或尾数)包含数字,作为自然数,指数指定点的左边(负指数)或右边(正指数)的点数应该转移。 JavaScript数字使用有理数作为有效数:1._f_其中_f_是52位小数。忽略符号,数字是有效数字乘以2_p_,其中_p_是指数(在稍后将解释的转换之后)。

比如:

| _f_ = %101, _p_ = 2 | Number: %1.101 × 22 = %110.1 | | _f_ = %101, _p_ = −2 | Number: %1.101 × 2−2 = %0.01101 | | _f_ = 0, _p_ = 0 | Number: %1.0 × 20 = %1 |

表示整数

整数的编码有多少位?有效数字有53个数字,一个在点之前,52个点。用_p_ = 52,我们有一个53位的自然数。唯一的问题是最高位始终为1.也就是说,我们没有全部位可供我们随意使用。分两步去除这个限制。首先,如果你需要一个最高位为0的53位数,然后是1,那么你设置_p_ = 51.分数的最低位成为该点之后的第一个数字,整数为0。依此类推,直到你处于编码数字1的_p_ = 0和_f_ = 0。

| | 52 | 51 | 50 | ... | 1 | 0 | (bits) | | p=52 | 1 | f51 | f50 | ... | f1 | f0 | | | p=51 | 0 | 1 | f51 | ... | f2 | f1 | f0=0 | | | ... | | p=0 | 0 | 0 | 0 | ... | 0 | 1 | f51=0, etc. |

其次,对于全部53位,我们仍然需要表示零。如何做到这一点在下一节中解释。请注意,由于符号是单独存储的,因此整数的幅度(绝对值)为53位。


指数

指数的长度是11位,这意味着它的最低值是0,最高值是2047(211-1)。为了支持负指数,使用所谓的偏移二进制编码:1023是零,所有较低数字都是负数,所有较高数字都是正数。这意味着你从指数中减去1023将其转换为正常数字。因此,我们以前使用的变量_p_等于_e_-1023,并且有效数字乘以2_e_-1023。

偏移量二进制编码中的一些数字:

    %00000000000     0  →  −1023  (lowest number)
    %01111111111  1023  →      0
    %11111111111  2047  →   1024  (highest number)

    %10000000000  1024  →      1
    %01111111110  1022  →     −1 

你倒置它的位并减1就能将一个数变为负数了。

特殊的指数

两个指数值是保留的:最低的一个(0)和最高的一个(2047)。 2047的指数用于无穷大和NaN(非数字)值[2]。 IEEE 754标准有许多NaN值,但JavaScript都将它们表示为单个值NaN。指数0用于两种能力。首先,如果分数也是0,那么整数就是0.由于符号是分开存储的,我们同时具有-0和+0(详见[3])。

其次,0的指数也用于表示非常小的数字(接近零)。然后该分数必须是非零的,如果是正数,则通过计算该数字

%0._f_ × 2−1022

这种表示是非规范化。先前讨论的表示被称为标准化。可以以规范化方式表示的最小的正数(非零)数是

%1.0 × 2−1022

最大的非正规化数字是

%0.1 × 2−1022

因此,在标准化和非标准化数字之间切换时没有漏洞。

总结:指数

| (−1)_s_ × %1._f_ × 2_e_−1023 | normalized, 0 < _e_ < 2047 | | (−1)_s_ × %0._f_ × 2_e_−1022 | denormalized, _e_ = 0, _f_ > 0 | | (−1)_s_ × 0 | _e_ = 0, _f_ = 0 | | NaN | _e_ = 2047, _f_ > 0 | | (−1)_s_ × ∞ (infinity) | _e_ = 2047, _f_ = 0 |

用_p_ = _e_ - 1023,指数的范围是

−1023 < _p_ < 1024


小数部分

并非所有小数都可以用JavaScript精确表示,如下所示:

    > 0.1 + 0.2
    0.30000000000000004

小数部分0.1和0.2都不能精确地表示为二进制浮点数。但是,与实际值的偏差通常太小而不能显示。加法导致偏差变得可见。另一个例子:

    > 0.1 + 1 - 1
    0.10000000000000009

表示0.1对于表示分数110来说是个挑战。困难的部分是分母10,其分母的因子分解是2×5.指数只允许你用2的幂除整数,所以没有办法得到5英寸。比较:13不能精确地表示为小数部分。它近似于0.333333 ...

相反,将二进制小数表示为小数部分总是可能的,您只需要收集足够多的二进制数(其中每十个都有一个)。例如:

%0.001 = 18 = 12 × 2 × 2 = 5 × 5 × 5(2×5) × (2×5) × (2×5) = 12510 × 10 × 10 = 0.125

比较小数部分

因此,当您使用具有小数值的小数输入时,不应直接比较它们。相反,考虑舍入误差的上限。这样的上限称为machine epsilon。双精度的标准epsilon值是2-53。

    var epsEqu = function () { // IIFE, keeps EPSILON private
        var EPSILON = Math.pow(2, -53);
return function epsEqu(x, y) {
            return Math.abs(x - y) < EPSILON;
};
}();

上述功能可确保在正常比较不充分的情况下获得正确结果:

    > 0.1 + 0.2 === 0.3
    false
    > epsEqu(0.1+0.2, 0.3)
    true

最大整数

如果有人说“_x_是最大整数”,这意味着什么?这意味着可以表示范围为0≤_n_≤_x_的每个整数_n_,并且对于大于_x_的任何整数都不能成立。 253符合该法案。以前的所有数字都可以表示:

    > Math.pow(2, 53)
    9007199254740992
    > Math.pow(2, 53) - 1
    9007199254740991
    > Math.pow(2, 53) - 2
    9007199254740990

但是下一个整数不能被表示:

    > Math.pow(2, 53) + 1
    9007199254740992

253的一些方面是上限可能是令人惊讶的。我们将通过一系列问题来看待他们。要记住的一件事是整数范围的高端限制资源是分数;指数仍有增长空间。

为什么是53位?您有53位可用于幅度(不包括符号),但分数只包含52位。这怎么可能?正如您在上面看到的那样,指数提供了第53位:它移动了分数,因此除零之外的所有53位数都可以表示,并且它有一个特殊值来表示零(连同零的一部分)。

为什么最高的整数不是253-1?通常,_x_位表示最低的数字是0,最高的数字是2_x_-1。例如,最高的8位数字是255.在JavaScript中,最高分数确实用于数字253-1,但可以表示253,这要归功于指数的帮助 - 它仅仅是一个分数_f_ = 0并且指数_p_ = 53(转换后):

%1._f_ × 2_p_ = %1.0 × 253 = 253

为什么高于253的数字可以代表?

示例:

    > Math.pow(2, 53)
    9007199254740992
    > Math.pow(2, 53) + 1  // not OK
    9007199254740992
    > Math.pow(2, 53) + 2  // OK
    9007199254740994

    > Math.pow(2, 53) * 2  // OK
    18014398509481984

253×2的作品,因为指数可以使用。每乘以2只是将指数递增1并且不影响分数。因此,就最大分数而言,乘以2的幂不是问题。为了明白为什么可以加2到253,而不是1,我们用前面的表扩展53和54的附加位,以及_p_ = 53和_p_ = 54的行:

| | 54 | 53 | 52 | 51 | 50 | ... | 2 | 1 | 0 | (bits) | | p=54 | 1 | f51 | f50 | f49 | f48 | ... | f0 | 0 | 0 | | | p=53 | | 1 | f51 | f50 | f49 | ... | f1 | f0 | 0 | | | p=52 | | | 1 | f51 | f50 | ... | f2 | f1 | f0 | |

查看行(_p_ = 53),应该很明显,JavaScript数字可以将位53设置为1.但是,因为分数_f_只有52位,所以位0必须为零。因此,只有偶数_x_可以在253≤_x_ <254范围内表示。在行(_p_ = 54)中,该间距增加到4的倍数,范围在254≤_x_ <255:

    > Math.pow(2, 54)
    18014398509481984
    > Math.pow(2, 54) + 1
    18014398509481984
    > Math.pow(2, 54) + 2
    18014398509481984
    > Math.pow(2, 54) + 3
    18014398509481988
    > Math.pow(2, 54) + 4
    18014398509481988

等等...

IEEE 754例外

IEEE 754标准描述了五个例外,其中一个不能计算精确的值:

  1. 无效:执行了无效操作。例如,计算负数的平方根。返回NaN [2]。

        > Math.sqrt(-1)
        NaN
    
    
  2. 除以零:返回正负无穷[2]。

        > 3 / 0
        Infinity
        > -5 / 0
        -Infinity
    
    
  3. 溢出:结果太大而无法表示。这意味着指数太高(_p_≥1024)。根据符号,有正面和负面溢出。返回正负无穷。

        > Math.pow(2, 2048)
        Infinity
        > -Math.pow(2, 2048)
        -Infinity
    
    
  4. 下溢:结果太接近零来表示。这意味着指数太低(_p_≤-1023)。返回非规格化的值或零。

         > Math.pow(2, -2048)
         0
    
    
  5. 不精确:操作产生了不准确的结果 - 要保留的分数有太多有效数字。返回一个舍入结果。

        > 0.1 + 0.2
        0.30000000000000004
    
        > 9007199254740992 + 1
        9007199254740992
    
    

#3和#4是关于指数,#5是关于分数。 #3和#5之间的区别非常微妙:在第五个例子中,我们超过了分数的上限(这将是整数计算中的溢出)。但只有超过指数的上限才称为IEEE 754中的溢出。


结论

在这篇博文中,我们研究了JavaScript如何将其浮点数转换为64位。它根据IEEE 754标准中的双精度进行。由于数字的显示方式,人们往往会忘记JavaScript不能精确地表示分母的因子分解包含2以外的数字的小数部分。例如,可以表示0.5(12),而0.6(35)不能表示。人们也往往忘记了三个组件符号,指数,一个数字的小数部分一起工作来表示一个整数。但是,当Math.pow(2,53)+ 2可以表示时,会遇到这种情况,但Math.pow(2,53)+ 1不能。

网页“IEEE-754 Analysis”允许您输入一个数字并查看其内部表示。


来源和相关阅读

这篇文章的来源:

This post is part of a series on JavaScript numbers, which includes:

  1. Displaying numbers in JavaScript
  2. NaN and Infinity in JavaScript
  3. JavaScript’s two zeros

原文链接: 2ality.com 

 

js常见知识点整理总结

一些常用的JavaScript 知识点整理,包括:两个函数是否等价、NaN是什么?它是什么类型?如何检测一个变量是否是NaN?作用域相关问题?js小数计算不准确的bug,js算法/思路相关,js类型强制转换

JavaScript中的魔幻代理Proxy

什么是 JavaScript 代理?通过 Proxy 我们可以拦截并改变一个对象的几乎所有的根本操作,包括但不限于属性查找、赋值、枚举、函数调用等等。利用 Proxy,我们可以拦截并不存在的属性的读取

适配器在JavaScript中的体现

适配器设计模式在JavaScript中非常有用,在处理跨浏览器兼容问题、整合多个第三方SDK的调用,都可以看到它的身影。适配器模式是一种软件设计模式,允许从另一个接口使用现有类的接口。它通常用于使现有的类与其他类一起工作,而无需修改其源代码。

JavaScript中的循环和作用域

JavaScript有一个特点,也许会让开发者头痛, 是与循环和作用域相关的.const。最简单的方案是用 let 声明、另外一个非常普遍的解决这个问题是使用pre-ES6代码, 同时它被称作即时调用函数表达式(IIFE)

JavaScript中的行为委托

行为委托简单来说就是一种设计模式,不同于传统的构造函数的类式设计。这里每个例子会通过构造函数,class和行为委托来不同实现,不过不会评论class,是否使用class取决于你的观点。

打造自己的JavaScript武器库

作为战斗在业务一线的前端,要想少加班,就要想办法提高工作效率。这里提一个小点,我们在业务开发过程中,经常会重复用到日期格式化、url参数转对象、浏览器类型判断、节流函数等一类函数,这些工具类函数

(...)这三个点在JavaScript中意味着什么?

解释JavaScript中三个点的作用:数组/对象扩展运算符、rest运算符(使用函数的参数时,无论是完全替换参数还是与函数的参数一起替换参数,这三个点也称为rest运算符)

await在forEach不起作用解决

我们知道await这个机制肯定是没问题的,如果真的有问题肯定不会轮到我测出来,那么其实剩下来的问题只能是for遍历的原因了。lodash的forEach和[].forEach不支持await,如果非要一边遍历一边执行await,可使用for-of

你不知道的 js 保留字

先是笼统的说一下有什么保留字,保留字的话根据犀牛书的划分,可有分为以下几类:基础保留字、严格模式下的保留字、严格模式下的不完全保留字、ECMAScript3的保留字、ECMAScipt 5 的保留字、全局变量和函数

js实现获取手机相册并上传

当初有很多人说使用form方法将文件封装来上传,可是因为要照顾到从相机中选择图片,所以一直没有去做。 后来看到了Uploader的方法来传文件,感觉自己找到了 。是使用plus.uploader来完成的