在現代 JavaScript 中,有兩種數字類型
-
JavaScript 中的常規數字儲存在 64 位元格式 IEEE-754 中,也稱為「雙精度浮點數」。這些數字是我們大部分時間使用的數字,我們將在本章中討論它們。
-
BigInt 數字表示任意長度的整數。有時需要它們,因為常規整數無法安全地超過
(253-1)
或小於-(253-1)
,如我們在本章 資料類型 中前面提到的。由於 bigint 在一些特殊領域中使用,因此我們將它們專門用於一個章節 BigInt。
所以這裡我們將討論常規數字。讓我們擴展對它們的了解。
更多撰寫數字的方法
想像一下,我們需要寫下 10 億。顯而易見的方法是
let billion = 1000000000;
我們也可以使用底線 _
作為分隔符號
let billion = 1_000_000_000;
這裡的底線 _
扮演「語法糖」的角色,讓數字更易於閱讀。JavaScript 引擎會忽略數字之間的 _
,因此它與上面的一百萬完全相同。
然而,在實際應用中,我們會盡量避免撰寫長串的零。我們太懶了。我們會嘗試寫一些像 "1bn"
表示十億或 "7.3bn"
表示七十三億。大多數大數字也是如此。
在 JavaScript 中,我們可以透過在數字後加上字母 "e"
並指定零的數量來縮短數字
let billion = 1e9; // 1 billion, literally: 1 and 9 zeroes
alert( 7.3e9 ); // 7.3 billions (same as 7300000000 or 7_300_000_000)
換句話說,e
會將數字乘以 1
並加上指定的零的數量。
1e3 === 1 * 1000; // e3 means *1000
1.23e6 === 1.23 * 1000000; // e6 means *1000000
現在讓我們寫一些非常小的數字。例如,1 微秒(一秒的百萬分之一)
let mсs = 0.000001;
就像之前一樣,使用 "e"
可以有所幫助。如果我們想避免明確寫出零,我們可以寫成
let mcs = 1e-6; // five zeroes to the left from 1
如果我們計算 0.000001
中的零,有 6 個。所以自然而然地,它就是 1e-6
。
換句話說,"e"
後面的負數表示除以 1 並加上指定的零的數量
// -3 divides by 1 with 3 zeroes
1e-3 === 1 / 1000; // 0.001
// -6 divides by 1 with 6 zeroes
1.23e-6 === 1.23 / 1000000; // 0.00000123
// an example with a bigger number
1234e-2 === 1234 / 100; // 12.34, decimal point moves 2 times
十六進位、二進位和八進位數字
十六進位數字廣泛用於 JavaScript 中表示顏色、編碼字元,以及許多其他用途。因此,自然而然地存在一種更簡短的寫法:0x
,然後是數字。
例如
alert( 0xff ); // 255
alert( 0xFF ); // 255 (the same, case doesn't matter)
二進位和八進位數字系統很少使用,但也可以使用 0b
和 0o
前綴來支援
let a = 0b11111111; // binary form of 255
let b = 0o377; // octal form of 255
alert( a == b ); // true, the same number 255 at both sides
只有 3 個數字系統具有這種支援。對於其他數字系統,我們應該使用函式 parseInt
(我們將在本章稍後看到)。
toString(base)
方法 num.toString(base)
會以具有給定 base
的數字系統中 num
的字串表示形式傳回。
例如
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
base
可以從 2
到 36
。預設值為 10
。
這方面的常見使用案例為
-
base=16 用於十六進位顏色、字元編碼等,數字可以是
0..9
或A..F
。 -
base=2 主要用於除錯位元運算,數字可以是
0
或1
。 -
base=36 是最大值,數字可以是
0..9
或A..Z
。整個拉丁字母用於表示數字。一個有趣但對36
有用的情況是,當我們需要將一個長的數字識別碼轉換成較短的識別碼時,例如,建立一個短網址。可以用 base36
的數字系統來表示它alert( 123456..toString(36) ); // 2n9c
請注意,在 123456..toString(36)
中的兩個點不是錯字。如果我們想直接在數字上呼叫一個方法,例如上面的範例中的 toString
,那麼我們需要在數字後面加上兩個點 ..
。
如果我們只放一個點:123456.toString(36)
,就會出現錯誤,因為 JavaScript 語法表示第一個點之後的小數部分。如果我們再放一個點,那麼 JavaScript 就知道小數部分是空的,現在進入方法。
也可以寫成 (123456).toString(36)
。
捨入
在使用數字時,最常用的運算之一是捨入。
有幾個內建函式可以進行捨入
Math.floor
- 向下捨入:
3.1
變成3
,-1.1
變成-2
。 Math.ceil
- 向上捨入:
3.1
變成4
,-1.1
變成-1
。 Math.round
- 捨入到最接近的整數:
3.1
變成3
,3.6
變成4
,中間值:3.5
也向上捨入到4
。 Math.trunc
(Internet Explorer 不支援)- 移除小數點後的所有內容,不捨入:
3.1
變成3
,-1.1
變成-1
。
以下是表格,用於總結它們之間的差異
Math.floor |
Math.ceil |
Math.round |
Math.trunc |
|
---|---|---|---|---|
3.1 |
3 |
4 |
3 |
3 |
3.6 |
3 |
4 |
4 |
3 |
-1.1 |
-2 |
-1 |
-1 |
-1 |
-1.6 |
-2 |
-1 |
-2 |
-1 |
這些函式涵蓋了處理數字小數部分的所有可能方式。但是,如果我們想將數字捨入到小數點後第 n
位數,該怎麼辦?
例如,我們有 1.2345
,並希望將其捨入到 2 位數,只取得 1.23
。
有兩種方法可以做到這一點
-
乘法和除法。
例如,要將數字捨入到小數點後第 2 位數,我們可以將數字乘以
100
,呼叫捨入函式,然後再除回去。let num = 1.23456; alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
-
方法 toFixed(n) 將數字捨入到小數點後第
n
位數,並傳回結果的字串表示形式。let num = 12.34; alert( num.toFixed(1) ); // "12.3"
這會四捨五入到最近的值,類似於
Math.round
let num = 12.36; alert( num.toFixed(1) ); // "12.4"
請注意,
toFixed
的結果是一個字串。如果小數部分比要求的短,零會附加到結尾let num = 12.34; alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits
我們可以使用一元加法或
Number()
呼叫將其轉換為數字,例如,寫入+num.toFixed(5)
。
不精確的計算
在內部,一個數字以 64 位元格式表示 IEEE-754,因此有正好 64 位元來儲存一個數字:其中 52 位元用於儲存數字,11 位元儲存小數點的位置,1 位元用於符號。
如果一個數字真的很大,它可能會溢位 64 位元的儲存空間,並變成一個特殊的數字值 Infinity
alert( 1e500 ); // Infinity
可能不太明顯,但經常發生的是精度的損失。
考慮這個(錯誤的!)相等性測試
alert( 0.1 + 0.2 == 0.3 ); // false
沒錯,如果我們檢查 0.1
和 0.2
的總和是否為 0.3
,我們得到 false
。
奇怪!如果不是 0.3
,那會是什麼?
alert( 0.1 + 0.2 ); // 0.30000000000000004
哎呀!想像一下,你正在製作一個電子商務網站,訪客將 $0.10
和 $0.20
的商品放入他們的購物車中。訂單總額將為 $0.30000000000000004
。這會讓任何人感到驚訝。
但為什麼會這樣?
一個數字以其二進位形式儲存在記憶體中,一個位元序列——一和零。但是像 0.1
、0.2
這樣的分數在十進位數字系統中看起來很簡單,實際上它們的二進位形式是無窮的分數。
alert(0.1.toString(2)); // 0.0001100110011001100110011001100110011001100110011001101
alert(0.2.toString(2)); // 0.001100110011001100110011001100110011001100110011001101
alert((0.1 + 0.2).toString(2)); // 0.0100110011001100110011001100110011001100110011001101
什麼是 0.1
?它是十進位 1/10
,十分之一。在十進位數字系統中,這樣的數字很容易表示。將其與三分之一進行比較:1/3
。它變成了一個無窮的分數 0.33333(3)
。
因此,在十進位系統中,除以 10
的次方保證能正常工作,但除以 3
則不行。出於同樣的原因,在二進位數字系統中,除以 2
的次方保證能正常工作,但 1/10
變成了一個無窮的二進位分數。
使用二進位系統根本無法準確儲存 0.1 或 0.2,就像無法將三分之一儲存為十進位分數一樣。
數字格式 IEEE-754 通過四捨五入到最接近的可能數字來解決這個問題。這些四捨五入規則通常不允許我們看到「微小的精度損失」,但它確實存在。
我們可以看到它的實際運作
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
當我們對兩個數字求和時,它們的「精度損失」會累加。
這就是為什麼 0.1 + 0.2
不完全等於 0.3
。
許多其他程式語言也存在相同的問題。
PHP、Java、C、Perl 和 Ruby 給出完全相同的結果,因為它們基於相同的數字格式。
我們可以解決這個問題嗎?當然,最可靠的方法是使用 toFixed(n) 方法來捨入結果。
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"
請注意,toFixed
始終會傳回字串。它確保小數點後有 2 位數。如果我們有電子商務並需要顯示 $0.30
,這實際上很方便。對於其他情況,我們可以使用一元加號將其強制轉換為數字。
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
我們也可以暫時將數字乘以 100(或更大的數字)將它們轉換為整數,進行運算,然後再除回去。然後,由於我們對整數進行運算,因此誤差會略微減少,但我們仍然會在除法中得到誤差。
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
因此,乘除法方法會減少誤差,但不會完全消除誤差。
有時我們可以嘗試完全避開分數。例如,如果我們在經營商店,那麼我們可以將價格儲存在美分而不是美元中。但是,如果我們應用 30% 的折扣怎麼辦?在實務上,完全避開分數很少見。在需要時,只需將它們捨入以去除「小數點後尾數」即可。
嘗試執行這個
// Hello! I'm a self-increasing number!
alert( 9999999999999999 ); // shows 10000000000000000
這會產生相同的問題:精度損失。數字有 64 位元,其中 52 位元可用於儲存數字,但這還不夠。因此,最低有效數字會消失。
JavaScript 在此類事件中不會觸發錯誤。它會盡力將數字放入所需的格式,但不幸的是,此格式不夠大。
數字內部表示的另一個有趣後果是存在兩個零:0
和 -0
。
這是因為符號由單一位元表示,因此可以設定或不設定任何數字,包括零。
在大多數情況下,這種區別並不明顯,因為運算子適合將它們視為相同。
測試:isFinite 和 isNaN
還記得這兩個特殊的數字值嗎?
Infinity
(和-Infinity
)是一個特殊的數字值,大於(小於)任何東西。NaN
代表錯誤。
它們屬於 number
類型,但不是「正常」數字,因此有特殊函式可以檢查它們。
-
isNaN(value)
將其參數轉換為數字,然後測試它是否為NaN
。alert( isNaN(NaN) ); // true alert( isNaN("str") ); // true
但我們需要這個函式嗎?我們不能只使用
=== NaN
比較嗎?很不幸地,不行。NaN
值的獨特之處在於它不等於任何東西,包括它自己。alert( NaN === NaN ); // false
-
isFinite(value)
將其參數轉換為數字,如果它是一個常規數字,而不是NaN/Infinity/-Infinity
,則傳回true
。alert( isFinite("15") ); // true alert( isFinite("str") ); // false, because a special value: NaN alert( isFinite(Infinity) ); // false, because a special value: Infinity
有時 isFinite
用於驗證字串值是否為一般數字
let num = +prompt("Enter a number", '');
// will be true unless you enter Infinity, -Infinity or not a number
alert( isFinite(num) );
請注意,在所有數字函式(包括 isFinite
)中,空白或僅包含空白的字串會被視為 0
。
Number.isNaN
和 Number.isFinite
Number.isNaN 和 Number.isFinite 方法是 isNaN
和 isFinite
函式的「更嚴格」版本。它們不會自動將其參數轉換為數字,而是檢查它是否屬於 number
類型。
-
如果參數屬於
number
類型且為NaN
,Number.isNaN(value)
會傳回true
。在任何其他情況下,它會傳回false
。alert( Number.isNaN(NaN) ); // true alert( Number.isNaN("str" / 2) ); // true // Note the difference: alert( Number.isNaN("str") ); // false, because "str" belongs to the string type, not the number type alert( isNaN("str") ); // true, because isNaN converts string "str" into a number and gets NaN as a result of this conversion
-
如果參數屬於
number
類型且不為NaN/Infinity/-Infinity
,Number.isFinite(value)
會傳回true
。在任何其他情況下,它會傳回false
。alert( Number.isFinite(123) ); // true alert( Number.isFinite(Infinity) ); // false alert( Number.isFinite(2 / 0) ); // false // Note the difference: alert( Number.isFinite("123") ); // false, because "123" belongs to the string type, not the number type alert( isFinite("123") ); // true, because isFinite converts string "123" into a number 123
在某種程度上,Number.isNaN
和 Number.isFinite
比 isNaN
和 isFinite
函式更簡單且更直接。但在實務上,isNaN
和 isFinite
最常被使用,因為它們的寫法較短。
Object.is
的比較有一個特殊的內建方法 Object.is
,它會像 ===
一樣比較值,但對於兩個邊界情況更可靠
- 它適用於
NaN
:Object.is(NaN, NaN) === true
,這很好。 - 值
0
和-0
不同:Object.is(0, -0) === false
,技術上來說這是正確的,因為數字在內部有一個符號位元,即使所有其他位元都是零,它也可能不同。
在所有其他情況下,Object.is(a, b)
與 a === b
相同。
我們在此提到 Object.is
,因為它經常在 JavaScript 規範中使用。當內部演算法需要比較兩個值是否完全相同時,它會使用 Object.is
(在內部稱為 SameValue)。
parseInt 和 parseFloat
使用加號 +
或 Number()
的數字轉換很嚴格。如果值不完全是數字,它就會失敗
alert( +"100px" ); // NaN
唯一的例外是字串開頭或結尾的空格,因為它們會被忽略。
但在現實生活中,我們經常有單位值,例如 CSS 中的 "100px"
或 "12pt"
。此外,在許多國家中,貨幣符號會出現在金額之後,因此我們有 "19€"
並希望從中提取數字值。
這就是 parseInt
和 parseFloat
的用途。
它們會從字串中「讀取」數字,直到無法讀取為止。如果發生錯誤,則會傳回已收集的數字。函式 parseInt
會傳回整數,而 parseFloat
會傳回浮點數
alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5
alert( parseInt('12.3') ); // 12, only the integer part is returned
alert( parseFloat('12.3.4') ); // 12.3, the second point stops the reading
有時候 parseInt/parseFloat
會傳回 NaN
。這會發生在無法讀取任何數字時
alert( parseInt('a123') ); // NaN, the first symbol stops the process
parseInt(str, radix)
的第二個參數parseInt()
函式有一個可選的第二個參數。它指定數字系統的基底,因此 parseInt
也能剖析十六進制數字、二進制數字等等的字串
alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, without 0x also works
alert( parseInt('2n9c', 36) ); // 123456
其他數學函式
JavaScript 有內建的 Math 物件,其中包含一個小型數學函式和常數程式庫。
幾個範例
Math.random()
-
傳回 0 到 1 之間的亂數(不含 1)。
alert( Math.random() ); // 0.1234567894322 alert( Math.random() ); // 0.5435252343232 alert( Math.random() ); // ... (any random numbers)
Math.max(a, b, c...)
和Math.min(a, b, c...)
-
傳回任意數量的參數中最大和最小的值。
alert( Math.max(3, 5, -10, 0, 1) ); // 5 alert( Math.min(1, 2) ); // 1
Math.pow(n, power)
-
傳回
n
乘以給定次方的值。alert( Math.pow(2, 10) ); // 2 in power 10 = 1024
Math
物件中還有更多函式和常數,包括三角函數,您可以在 Math 物件的說明文件 中找到。
摘要
要寫入有許多零的數字
- 在數字後加上
"e"
和零的數量。例如:123e6
和123
加上 6 個零123000000
相同。 "e"
後面為負數會讓數字除以 1 和給定的零。例如:123e-6
表示0.000123
(123 百萬分之一)。
對於不同的數字系統
- 可以直接寫入十六進制(
0x
)、八進制(0o
)和二進制(0b
)系統的數字。 parseInt(str, base)
會將字串str
剖析成給定base
的數字系統中的整數,2 ≤ base ≤ 36
。num.toString(base)
會將數字轉換成給定base
的數字系統中的字串。
對於一般數字測試
isNaN(value)
將其參數轉換為數字,然後測試它是否為NaN
。Number.isNaN(value)
會檢查其參數是否屬於number
類型,如果是,則測試它是否為NaN
isFinite(value)
會將其參數轉換成數字,然後測試它是否不是NaN/Infinity/-Infinity
Number.isFinite(value)
會檢查其參數是否屬於number
類型,如果是,則測試它是否不是NaN/Infinity/-Infinity
對於將 12pt
和 100px
等值轉換成數字
- 對於「軟性」轉換,請使用
parseInt/parseFloat
,它會從字串中讀取數字,然後傳回在錯誤發生前可以讀取的值。
對於分數
- 使用
Math.floor
、Math.ceil
、Math.trunc
、Math.round
或num.toFixed(precision)
進行四捨五入。 - 務必記住,在處理分數時會產生精確度損失。
更多數學函數
- 當您需要時,請參閱 Math 物件。此函式庫非常小,但可以滿足基本需求。
留言
<code>
標籤,對於多行 – 將它們包在<pre>
標籤中,對於超過 10 行 – 使用沙盒 (plnkr、jsbin、codepen…)