JavaScript是怎樣編碼數字的[How numbers are encoded in JavaScript]

在JavaScript中全部的數字都是浮點數,本篇文章將介紹這些浮點數在JavaScript內部是怎樣被轉爲64位二進制的。
咱們會特別考慮整數的處理,因此讀完本篇以後,你會理解爲何會有如下結果發生:html

> 9007199254740992 + 1
9007199254740992
> 9007199254740992 + 2
9007199254740994

1. JavaScript的數字

JavaScript數字所有是浮點數。 根據 IEEE 754標準中的64位二進制(binary64), 也稱做雙精度規範(double precision)來儲存。從命名中能夠看出,這些數字將以二進制形式,使用64個字節來存儲。這些字節按照如下規則分配:編碼

0 - 51 字節是 分數f(fraction )
52 - 62 字節是 指數(exponent )
63 字節 是 標誌位 (sign)
標誌位 (s, sign) 指數(e, exponent ) 分數(f, fraction )
(1 bit) (11 bit) (52 bit)
63 62 51
52 0

他們按照如下規則表示一個數字: 若是標誌位是0, 表示這個數字爲正數,不然爲負數。粗略來講,分數f用來表示數字的‘數碼’(0-9),指數表示這個數字的‘點’在哪裏。接下來咱們會使用二進制(雖然這並非一般的浮點數表示方式)。並用一個%做爲前綴來標識。雖然JavaScript數字是以二進制保存的,但輸出(打印)時一般是以10進制顯示. 接下來的例子,咱們也會沿用這一規則。.net

2. 分數f

下表是一種表示非負浮點數的方法:
尾數 (小數點後面的數,significand 或 mantissa ) 以天然數字的形式保存‘數碼’,指數決定須要往左(負指數)或者右(正指數)移多少位。再忽略位數,這個JavaScript數字就是 有理數1.f乘以2p。
譯者注: 這裏指數用p而不是e來表示是由於e是一個偏移量,第三點會詳細說明code

好比如下例子:orm

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

2.1 表示一個整數

須要多少位來編碼一個整數呢? 尾數共有53個數碼,1個在‘點’的前面,52個在後面,若是p=52,咱們就有一個53位的天然數,如今的問題是最高位老是爲1,也就是說咱們不能隨便的使用全部的位。要去掉這個限制,咱們須要2步,首先. 若是須要最高位是0,第二位是1的53位的數字,將p設置爲51,這時分數f最低位變成了‘點’後面的第一個數碼,也就是整數0。按照這個規律,直到指數p=0,分數f=0,這就是數字1的編碼。htm

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位數字,咱們還須要表示0,咱們將在下一段詳細介紹。
須要注意的是,咱們能夠表示完整的53位整數,由於標誌位是另外儲存的。ip

3. 指數e

指數佔11位,它能夠表示0-2047(211-1), 爲了支持負指數,JavaScript使用偏移二進制來編碼: 1023表示0,小於它的爲負,大於它的爲正。這就意味着,減去1023才能獲得正常點數字。所以咱們以前使用的變量p就等於e-1023,也就是尾數乘以2e-1023
例如:ci

%00000000000     0  →  −1023  (最小的數字)
    %01111111111  1023  →      0
    %11111111111  2047  →   1024  (最大的數字)
                         
    %10000000000  1024  →      1
    %01111111110  1022  →     −1

若是須要一個負數,只須要顛倒一下它的位數,再減一資源

3.1 特殊的指數

有2個指數是保留位。最小的0,和最大的2047. 指數2047表示無窮大(infinity)和 NaN(非數字)值。IEEE 754標準有不少非數字值, 可是JavaScript把他們都表示爲NaN。指數爲0時有兩個意思。1. 若是分數f也是0,表示這個數字就是0.由於標誌位是單獨存儲的。因此咱們有+0和-0;get

而後指數0也能夠用來表示很是小的數字(接近0)。此時分數f必須爲非0,並且,若是這個數字是由%0.f × 2−1022算出來的,這個表示方式叫作非規範化,而以前咱們討論的表示方式叫規範化。最小的非0正數能夠被規範化爲: %1.0 × 2−1022。 最大的非規範化數字爲: %0.1 × 2−1022, 因此,從規範化到非規範化是過渡是平滑的。
譯者注: 規範化就是把小數點放在第一個非零數字的後面

3.2 總結:

(−1)s × %1.f × 2e−1023 normalized, 0 < e < 2047
(−1)s × %0.f × 2e−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

4. 十進制分數

不是全部的十進制分數都可以很是精確的表示, 例如:

> 0.1 + 0.2
0.30000000000000004

0.1和0.2都不可以被精確的表示成二進制浮點數。可是這個誤差一般很是很是小,小到不可以被表示出來,加法可使這個誤差變得可見:

> 0.1 + 1 - 1
0.10000000000000009

表示0.1至關於表示一個分數110,難的部分在於分母是10,10素數分解是2*5. 而指數只能分解2,因此沒有辦法獲得5。相同的, 1/3也不能被精確表示成一個十進制分數,它大概能被表示成0.333333。
但相對的。要用十進制表示一個2進制分數倒是永遠可行的,值須要使用足夠的2(每一個10都有1個2)。

%0.001 = 1/8 = 1/2 × 2 × 2 = 5 × 5 × 5/(2×5) × (2×5) × (2×5) = 125/10 × 10 × 10 = 0.125

4.1 對比十進制分數

所以,當你要處理10進制分數,不要直接去比較他們,先想想,它可能會有一個上限,好比有一個上限叫作機器最小數 machine 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

5. 最大的整數

「x 是最大的整數」這句話是什麼意思呢?它的意思是說,任意整數n在 0 ≤ n ≤ x 範圍內都是能夠被表示的。也就是說若是大於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 這個上限,有一些很使人驚奇的表現。咱們將用一些問題來解釋這些現象。你要記住的是,這個上限是分數f的上限,指數e部分其實還有空間。

爲何是53位呢?你有53位來表示數的大小,除去標誌位。可是分數f倒是由52位組成的,這是爲何呢。從前面的文章能夠看出,指數e從第53位開始,它會移動分數f,因此這個53位的數字(除了0)能夠被表示出來,而且有一個特別的數字去表示0(而且分數f也是0).

爲何最大的數不是253−1? 一般來講,x位就說明最小數是0,最大值是2x−1. 好比8位數字最大是255。而在JavaScript裏,最大的分數f確實是253−1,但253 也能夠被表示出來,由於有指數e的幫助。它只要讓分數f等於0,指數e等於53便可。

%1.f × 2p = %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 能夠表示正確,由於指數e還能夠用,乘以2僅僅須要指數e加一,而不影響分數f。因此乘以2的冪不是問題,只要分數f沒有超過上限,那爲何2加253也能夠表示正確,1卻不能夠呢,咱們擴大一下以前的,加上53 和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位必須位0,而只有253 ≤ x < 254中的偶數數字x能夠被表示。在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

6. IEEE 754 的例外

IEEE 754標準描述了5中例外 , 當出現這些例外,就沒法算出準確的數字。

1. 無效 : 進行一個無效操做。例如,給一個負數開平方,返回NaN

> Math.sqrt(-1)
NaN

2. 除以0 : 返回正或者負的infinity(無窮大)

> 3 / 0
Infinity
> -5 / 0
-Infinity

3. 溢出(overflow) : 結果太大,沒法表示。這時是指數已經太大, (p ≥ 1024).根據標誌位,正或者負溢出,返回正或者負的infinity(無窮大)。

> Math.pow(2, 2048)
Infinity
> -Math.pow(2, 2048)
-Infinity

4. 潛流(underflow): 結果太接近於0,這時是指數已經過小(p ≤ −1023). 返回一個非規範化的數字,或者0.

> Math.pow(2, -2048)
0

5. 不精確(Inexact): 一個操做返回不精確的結果 - 有太多有意義的數字須要分數f去存,那就返回一個四捨五入的結果

> 0.1 + 0.2
0.30000000000000004
    
> 9007199254740992 + 1
9007199254740992

上面的第三點和第四點是關於指數的,第五點是關於分數f的,第三點和第五點的差異很是小,第五點的第二個例子,咱們已經接近了分數f的最大值(這也能夠算是一個溢出操做)。但根據 IEEE 754只有超過了指數的範圍纔算溢出。

7. 結論

本篇文章中,咱們觀察了JavaScript是怎樣把浮點數存進64位中的。它之因此這麼作是根據 IEEE 754 標準中的雙精度。由於咱們經常忘記,JavaScript對於分母質因分解不只包含2的數字 是沒法精確表示的。好比0.5(1/2),是能夠精確表示的,但0.6(3/5)就不能。咱們很容易忘記一個整數是由標誌位,分數f,指數3部分組成,而後就會面對Math.pow(2, 53) + 2 能夠計算正確,而Math.pow(2, 53) + 1會計算錯誤的問題。

8. 資源和引用

• 「IEEE Standard 754 Floating-Point」 - Steve Hollasch.
• 「Data Types and Scaling (Fixed-Point Blockset)」 in the MATLAB documentation.
• 「IEEE 754-2008」 on Wikipedia

本文也同時是JavaScript 數字系列 , 它包含:

  1. JavaScript中的數字顯示
  2. JavaScript中的NaN 和 Infinity
  3. JavaScript的兩種0
相關文章
相關標籤/搜索