你對Number一無所知

二進制表示小數

例如用二進制表示 0.8125javascript

0.8125
0.8125*2 = 1.625 取整爲 1
0.625*2=1.25 取整爲 1
0.25*2=0.5 取整爲 0
0.5*2=1 取整爲 1
如果 *2 始終沒法獲得 1,就一直到位數用完,這也是浮點數並不精確的緣由
即 0.8125 的二進制表示是 0.1101
即 0.8125 = 2^-1*1+2^-2*1+2^-3*0+2^-4*1
即 0.8125 = 0.5 + 0.25 + 0.0625

因此 0.1 到 0.9 的 9 個小數中,只有 0.5 能夠用二進制精確表示html

浮點數與定點數

在 JS 中,全部的數字都是基於 IEEE 754 的浮點數。除了浮點數,還有定點數,二者的區別就在於小數點的處理。一樣是用64個bit表示一個數,定點數會用前 N 位來表示一個數的整數部分,用後 64 - N 來表示一個數的小數部分,這個 N 是固定的,對全部的數都是同樣的。java

64位浮點數

對於64位的浮點數,最高的1位是符號位 S,接着是11位的指數 E,剩下的52位爲有效數字 Mgit

bg2010060602

S,Sign(1bit):表示浮點數是正數仍是負數。0表示正數,1表示負數。程序員

E,Exponent(11bit):指數部分。相似於科學記數法的 M*10^N 中的 N,只不過不是以10爲底,而是以2爲底。E是一個無符號整數,由於長度是11位,取值範圍是 0~2047。可是科學計數法中的指數是能夠爲負數的,因此再減去一箇中間數 1023,[0,1022]表示爲負,[1024,2047] 表示爲正github

M,Mantissa(52bit):基數部分。浮點數具體數值的實際表示。小程序

因此計算機將 5.8125 存儲爲浮點數的過程以下app

  1. 用二進制表示5.8125,獲得 101.1101
  2. 用相似科學計數法表示二進制數 101.1101,獲得 1.011101 * 2 ^ 2
  3. 偏移指數部分,在 5.8125 中,指數爲2,是正數,但實際上指數也可能爲負數,例如若是是0.8125,指數就爲-1了。11位的指數部分,爲了包括負數,全部須要偏移 2^10-1,即 1023。因此指數偏移後爲1025=2+1023,用二進制表示 10000000001
  4. 處理整數部分,用相似科學計數法表示二進制數後,小數點前面總爲1,能夠將這個1忽略,這樣能夠增長表示數的範圍

綜上,5.8125 在計算機中的存儲形式 0(1位) + 0111,1111,111(11位) + 011101(6位) +0…0(46位)工具

M部分是二進制表示數,E控制小數點的位置,S控制數的正負測試

如今開始解釋 Number 中的一些事

JS 中全部的數字都是浮點數,64位浮點數能準確表示的實數是有限的,例如以前提到, 0.1 到 0.9 的 9 個小數中,只有 0.5 能夠用浮點數準確表示

687474703a2f2f617461322d696d672e636e2d68616e677a686f752e696d672d7075622e616c6979756e2d696e632e636f6d2f65656539613263613238646433643865366630663563383939353661623433612e6a7067

盜圖來源JavaScript浮點數陷阱及解法

也由於 JS 中全部的數字都是以浮點數的形式保存的,因此數字後面的「.」會被看成小數點來處理

因此有

var a = 0.42;        // 0.42
var b = .42;        // 0.42
42.toFixed( 3 );    // SyntaxError
(42).toFixed( 3 );    // "42.000"
0.42.toFixed( 3 );    // "0.420"
42..toFixed( 3 );    // "42.000"

Number.MAX_VALUE 是如何得來的

64位浮點數,M部分最大爲52個1 + 被忽略的1,E部分最大爲 2046-1023 = 1023,

爲何不是 2047-1023 = 1024,由於這個1024要用來表示Infinity

因此最大的二進制數位 1.1111…1(小數點後面52個1) * 2 ^ 1023

在計算機中存儲形態爲 0 | 111 1111 1110 | 111111...111(52個1)

let sum = 0
for (let i = 0; i < 53; i++) {
    sum = sum + Math.pow(2, 1023 - i)
    console.log(sum)
}
sum // 1.7976931348623157e+308
Number.MAX_VALUE // 1.7976931348623157e+308
// 若是把代碼改爲 i < 53,獲得 1.7976931348623155e+308

Number.MIN_VALUE 是如何得來的

這個值是 最接近0且大於0的,且是精確表示的數

64位浮點數,M部分最小爲0 + 被忽略的1,E部分最小爲 0-1023 = -1023,

1*2^-1023,即二進制 0.00…001(1以前有1023個0)

Math.pow(2, -1023) // 1.1125369292536007e-308
Number.MIN_VALUE // 5e-324

嗯?WHY?

「若是浮點數的指數部分的編碼值是0,尾數爲非零,那麼這個浮點數將被稱爲非規約形式的浮點數。IEEE 754標準規定:非規約形式的浮點數的指數偏移值比規約形式的浮點數的指數偏移值大1.例如,最小的規約形式的單精度浮點數的指數部分編碼值爲1,指數的實際值爲-126;而非規約的單精度浮點數的指數域編碼值爲0,對應的指數實際值也是-126而不是-127。實際上非規約形式的浮點數仍然是有效可使用的,只是它們的絕對值已經小於全部的規約浮點數的絕對值;即全部的非規約浮點數比規約浮點數更接近0。規約浮點數的尾數大於等於1且小於2,而非規約浮點數的尾數小於1且大於0.

...

除了規約浮點數,IEEE754-1985標準採用非規約浮點數,用來解決填補絕對值意義下最小規格數與零的距離。(舉例說,正數下,最大的非規格數等於最小的規格數。而一個浮點數編碼中,若是exponent=0,且尾數部分不爲零,那麼就按照非規約浮點數來解析)非規約浮點數源於70年代末IEEE浮點數標準化專業技術委員會醞釀浮點數二進制標準時,Intel公司漸進式下溢出(gradual underflow)的力薦。當時十分流行的DEC VAX機的浮點數表示採用了忽然式下溢出(abrupt underflow)。若是沒有漸進式下溢出,那麼0與絕對值最小的浮點數之間的距離(gap)將大於相鄰的小浮點數之間的距離。例如單精度浮點數的絕對值最小的規約浮點數是1.0times 2^{{-126}},它與絕對值次小的規約浮點數之間的距離爲2^{{-126}}times 2^{{-23}}=2^{{-149}}。若是不採用漸進式下溢出,那麼絕對值最小的規約浮點數與0的距離是相鄰的小浮點數之間距離的2^{{23}}倍!能夠說是很是忽然的下溢出到0。這種狀況的一種糟糕後果是:兩個不等的小浮點數X與Y相減,結果將是0.訓練有素的數值分析人員可能會適應這種限制狀況,但對於普通的程序員就很容易陷入錯誤了。採用了漸進式下溢出後將不會出現這種狀況。例如對於單精度浮點數,指數部分實際最小值是(-126),對應的尾數部分從1.1111ldots 11,1.1111ldots 10一直到0.0000ldots 10, 0.0000ldots 010.0000ldots 00相鄰兩小浮點數之間的距離(gap)都是2^{{-126}}times 2^{{-23}}=2^{{-149}};而與0最近的浮點數(即最小的非規約數)也是2^{{-126}}times 2^{{-23}}=2^{{-149}}。「

以上文字來源非規約形式的浮點數

總結來講,規定當浮點數的指數爲容許的最小指數值,尾數沒必要是規範化的。

M部分最小爲51個0和1個1

因此最小二進制數,0.00….001(小數點後面51個0)*2^-1022,即二進制0.00..001(1以前有52+1022個0)

Math.pow(2, -1074) // 5e-324

Number.MAX_SAFE_INTEGER

從 Number.MIN_SAFE_INTEGER 到 Number.MAX_SAFE_INTEGER 之間連續的整數都是能夠準確表示的

E爲52,算上偏移+1023,爲1075,用二進制表示 10000110011

M爲52個1,算上默認的1,爲1.11….1(小數點後面52個1)

在計算機中存儲形態爲 0 | 100 0011 0011 | 111111...111(52個1)

總體來看就是 1.11…1(小數點後面52個1)*2^52,即 53個1

let sum = 0
for (let i = 0; i < 53; i++) {
    sum = sum + Math.pow(2, i)
}
sum // 9007199254740991
Number.MAX_SAFE_INTEGER // 9007199254740991

Number.MIN_SAFE_INTEGER

將 S 位表示爲1

在計算機中存儲形態爲 1 | 100 0011 0011 | 111111...111(52個1)

0.1 + 0.2 === 0.3 // false

0.1 轉成二進制 0.000110011001100...

即 1.1001100 * 2^-4

指數位偏移 1019 = -4+1023,用二進制表示 1111 1110 11

0.1 在計算機中存儲形態 0 | 011 1111 1011|1001 1001 1001 1001 1001(12個1001) 1010

最後四位不是 1001 而是 1010,是考慮到 1001的下一位是1,故進一位

再將其轉換成二進制 1.1001 1001 1001(12個1001)1010 * 2^-4

其實呢

0.1.toString(2) // "0.0001100110011001100110011001100110011001100110011001101"

0.2 轉成二進制 0.0011001100110011...

即 1.1001 1001…*2^-3

指數位偏移 1020 = -3+1023,用二進制表示 1111 1111 00

0.2 在計算機中存儲形態 0 | 011 1111 1100 | 1001 1001 1001 1001 1001(12個1001) 1010

最後四位不是 1001 而是 1010,是考慮到 1001的下一位是1,故進一位

再將其轉換成二進制 1.1001 1001 1001(12個1001) 1010 *2^-3

0.2.toString(2) // "0.001100110011001100110011001100110011001100110011001101"

將這兩個二進制數相加

寫個小程序計算一下

function addBinary(a, b) {
    let aStr = a,
        bStr = b
    let addLength = aStr.length - bStr.length
    if (addLength > 0) {
        bStr = bStr + '0'.repeat(addLength)
    } else if (addLength < 0) {
        aStr = aStr + '0'.repeat(-addLength)
    }
    let addFlag = 0
    let arr1 = [...aStr]
    let arr2 = [...bStr]
    let length = arr1.length
    let arr3 = []
    for (let i = 0; i < length; i++) {
        let el1 = arr1.pop()
        let el2 = arr2.pop()
        if (el1 * 1 + el2 * 1 === 0) {
            arr3.unshift(addFlag)
            addFlag = 0
        } else if (el1 * 1 + el2 * 1 === 1) {
            if (addFlag === 1) {
                arr3.unshift(0)
                addFlag = 1
            } else {
                arr3.unshift(1)
                addFlag = 0
            }
        } else if (el1 * 1 + el2 * 1 === 2) {
            arr3.unshift(addFlag)
            addFlag = 1
        }
    }
    return arr3.join('')
}
// 參數a,b 爲小數後面的部分
addBinary('0001100110011001100110011001100110011001100110011001101','001100110011001100110011001100110011001100110011001101')
// 0100110011001100110011001100110011001100110011001100111

加上整數部分,獲得 0.0100110011001100110011001100110011001100110011001100111

將其轉換爲實數

function convertBinary(a) {
    return [...a].reduce((acc, cur, i) => {
        acc = acc + cur * Math.pow(2, -1 * (i + 1))
        return acc
    }, 0)
}
// 參數a 依然爲小數後面的部分
convertBinary('0100110011001100110011001100110011001100110011001100111')
// 0.30000000000000004

Number.EPSILON

if (!Number.EPSILON) {
    Number.EPSILON = Math.pow(2,-52);
}

用 Number.EPSILON(容差) 來比較兩個 number 的等價性

The value of Number.EPSILON is the difference between 1 and the smallest value greater than 1 that is representable as a Number value, which is approximately 2.2204460492503130808472633361816 x 10‍−‍16.

根據ECMASCRIPT-262定義 Number.EPSILON 是大於1的最小可表示數與1的差

function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

numbersCloseEnoughToEqual( a, b );                    // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 );    // false

Infinity, a + 1 === a

根據 IEEE 754

形式 指數 小數部分
0 0 0
非規約形式 0 非0
規約形式 1到2^e-2 任意
2^e-1 0
NaN 2^e-1 非0
  1. 若是指數是0而且尾數的小數部分是0,這個數±0(和符號位相關)
  2. 若是指數 =2^{{e}}-1而且尾數的小數部分是0,這個數是±(一樣和符號位相關)
  3. 若是指數 = 2^{{e}}-1而且尾數的小數部分非0,這個數表示爲不是一個數(NaN)
var a = Number.MAX_VALUE;    // 1.7976931348623157e+308
a + 1 === a;                // true
a + a;                        // Infinity
a + Math.pow( 2, 970 );        // Infinity
a + Math.pow( 2, 969 );        // 1.7976931348623157e+308

IEEE 754

Infinity 中 E的二進制表示爲 111 1111 1111,M爲1(默認) + 52個0

Infinity 在計算機中存儲形態 0 | 111 1111 1111 | 0000(52個0)

Math.pow(2, 1024) // Infinity
Math.pow(2, 1023) + Math.pow(2, 1022) + ... + Math.pow(2, 971) // 1.7976931348623157e+308

能夠看到 InfinityNumber.MAX_VALUE 之間相差 Math.pow(2, 971)

Untitled Diagram

IEEE 754 「就近舍入」,Number.MAX_VALUE + Math.pow( 2, 969 ) 比起 Infinity 更接近於 Number.MAX_VALUE,因此它「向下舍入」,而 Number.MAX_VALUE + Math.pow( 2, 970 ) 距離 Infinity 更近,因此它「向上舍入」。

再個人理解看來,凡是大於等於 Number.MAX_VALUE + Math.pow( 2, 970 ) 的數字都用 Infinity 來存儲

存儲形態都是 0 | 111 1111 1111 | 0000(52個0)

Number.MAX_VALUE + Math.pow(2,971) === Number.MAX_VALUE + Math.pow(2,972) // true

NaN

首先 typeof NaN 返回 number,NaN表示不是數字的數字

NaN 爲數字表現爲它來源於數學計算,

NaN 不是數字表現爲計算過程當中的參數並不符合要求,致使計算結果不是數字

"foo"/"foo" // NaN
1 * "fp" // NaN
1 / 0    // Infinity
0 / 0    // NaN
Infinity / Infinity // NaN
Infinity / 1 // Infinity
Infinity / 0 // Infinity

如何判斷一個數值是 NaN

  • typeof n === "number" && window.isNaN(n)
  • n !== n

    • 在整個語言中 NaN 是惟一一個本身與本身不相等的值
  • Number.isNaN(n)

0 & -0

Object.is(0, -0);            // false
Object.is(-0, -0);           // true
Object.is(NaN, 0/0);         // true
function fakeIs(v1, v2) {
    // 測試 `-0`
    if (v1 === 0 && v2 === 0) {
        return 1 / v1 === 1 / v2;
    }
    // 測試 `NaN`
    if (v1 !== v1) {
        return v2 !== v2;
    }
    // 其餘狀況
    return v1 === v2;
}

Number 的方法

  • numObj.toExponential(fractionDigits)

    • fractionDigits 規定了小數位的位數
    • 以指數形式展示數字,科學計數法
  • numObj.toFixed(digits)

    • digits 規定了小數位的位數,不足用 0 填充
    • 固定小數位數
  • numObj.toPrecision(precision)

    • precision 規定了整數位+小數位的位數
    • 以固定精度返回數字

參考博客

工具網站

相關文章
相關標籤/搜索