JavaScript 中的所有数字都是浮点数. 这篇博客文章解释了这些浮点数如何在 64 位二进制内部表示. 由于特别考虑, 本文中的数字将用整数表示, 以便在阅读本文后, 您将了解在以下交互中会发生什么:
- (译者注: 浮点数并不一定等于小数, 定点数也并不一定就是整数. 所谓浮点数就是小数点在逻辑上是不固定的, 而定点数只能表示小数点固定的数值, 具用浮点数或定点数表示某哪一种数要看用户赋予了这个数的意义是什么.)
- > 9007199254740992 + 1
- 9007199254740992
- > 9007199254740992 + 2
- 9007199254740994
JavaScript 数字
JavaScript 数字都是浮点数, 按照 IEEE 754 standard http://en.wikipedia.org/wiki/IEEE_754 标准进行存储. 该标准有几种格式. 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 × 22 = %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). 为了支持负指数, 使用所谓的偏移二进制 http://en.wikipedia.org/wiki/Offset-binary 编码: 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_ × 21022
这种表示是非规范化. 先前讨论的表示被称为标准化. 可以以规范化方式表示的最小的正数 (非零) 数是
%1.0 × 21022
最大的非正规化数字是
%0.1 × 21022
因此, 在标准化和非标准化数字之间切换时没有漏洞.
总结: 指数
| (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 http://en.wikipedia.org/wiki/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 标准描述了五个例外, 其中一个不能计算精确的值:
无效: 执行了无效操作. 例如, 计算负数的平方根. 返回 NaN [2].
- > Math.sqrt(-1)
- NaN
除以零: 返回正负无穷[2].
- > 3 / 0
- Infinity
- > -5 / 0
- -Infinity
溢出: 结果太大而无法表示. 这意味着指数太高(_p_1024). 根据符号, 有正面和负面溢出. 返回正负无穷.
- > Math.pow(2, 2048)
- Infinity
- > -Math.pow(2, 2048)
- -Infinity
下溢: 结果太接近零来表示. 这意味着指数太低(_p_-1023). 返回非规格化的值或零.
- > Math.pow(2, -2048)
- 0
不精确: 操作产生了不准确的结果 - 要保留的分数有太多有效数字. 返回一个舍入结果.
- > 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 http://babbage.cs.qc.edu/IEEE-754/" 允许您输入一个数字并查看其内部表示.
- "Data Types and Scaling (Fixed-Point Blockset) http://radio.feld.cvut.cz/matlab/toolbox/fixpoint/c3_bev12.html" in the MATLAB documentation.
- "IEEE 754-2008 http://en.wikipedia.org/wiki/IEEE_754" on Wikipedia.
- This post is part of a series http://2ality.com/archive.html?tag=numbers on JavaScript numbers, which includes:
来源: https://juejin.im/entry/5afa9df9f265da0b71566c7c