在現代 JavaScript 中,有兩種類型的數字:
JavaScript 中的常規數字以 64 位元格式 IEEE-754 存儲,也稱為「雙精度浮點數」。這些是我們大多數時間使用的數字,我們將在本章中討論它們。
BigInt 數字表示任意長度的整數。有時需要它們,因為常規整數不能安全地超過(2 53 -1)
或小於-(2 53 -1)
,正如我們前面在資料類型一章中提到的。由於 bigint 被用在一些特殊的領域,我們將它們專門放在一個專門的章節 BigInt 中。
所以在這裡我們將討論常規數字。讓我們擴大對它們的了解。
想像一下我們需要寫 10 億。顯而易見的方法是:
讓十億= 1000000000;
我們也可以使用底線_
作為分隔符號:
讓十億 = 1_000_000_000;
這裡下劃線_
扮演了「語法糖」的角色,它使數字更具可讀性。 JavaScript 引擎只是忽略數字之間的_
,所以它與上面的 10 億完全相同。
但在現實生活中,我們盡量避免寫長的零序列。我們太懶了。我們會試著寫"1bn"
來代表10億,或是"7.3bn"
來代表70億3億。對於大多數大數來說也是如此。
在 JavaScript 中,我們可以透過在數字後面附加字母"e"
並指定零數來縮短數字:
讓 10 億 = 1e9; // 10 億,字面意思:1 和 9 個零 警報(7.3e9); // 73 億(與 7300000000 或 7_300_000_000 相同)
換句話說, e
將數字乘以1
並給出給定的零計數。
1e3 === 1 * 1000; // e3 表示 *1000 1.23e6 === 1.23 * 1000000; // e6 表示 *1000000
現在讓我們寫一些非常小的東西。假設 1 微秒(百萬分之一秒):
令 mсs = 0.000001;
就像以前一樣,使用"e"
會有所幫助。如果我們想避免明確地寫入零,我們可以這樣寫:
讓 mcs = 1e-6; // 1 左邊五個零
如果我們計算0.000001
中的零,則有 6 個。所以自然是1e-6
。
換句話說, "e"
後面的負數表示除以 1 並帶有給定數量的零:
// -3 除以 1,有 3 個零 1e-3 === 1 / 1000; // 0.001 // -6 除以 1,有 6 個零 1.23e-6 === 1.23 / 1000000; // 0.00000123 // 一個更大數字的例子 1234e-2 === 1234 / 100; // 12.34,小數點移動2次
十六進位數字在 JavaScript 中廣泛用於表示顏色、編碼字元以及許多其他用途。因此,自然地,存在一種更短的書寫方式: 0x
,然後是數字。
例如:
警報(0xff); // 255 警報(0xFF); // 255(相同,大小寫無關)
二進制和八進制數字系統很少使用,但也支援使用0b
和0o
前綴:
令a = 0b11111111; // 255 的二進位形式 令b = 0o377; // 255 的八進位形式 警報(a==b); // true,兩邊的數字相同255
只有 3 個數字系統具有這種支援。對於其他數字系統,我們應該使用函數parseInt
(我們將在本章後面看到)。
方法num.toString(base)
傳回具有給定base
的數字系統中num
的字串表示形式。
例如:
讓數字= 255; 警報(num.toString(16)); // ff 警報(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
的一個有趣但有用的情況是,當我們需要將長數字標識符轉換為較短的標識符時,例如,創建一個短 url。可以簡單地用以36
為基數的數字系統來表示:
警報(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
, -3.5
向上舍入為-3
。
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.5 | 3 | 4 | 4 | 3 |
3.6 | 3 | 4 | 4 | 3 |
-1.1 | -2 | -1 | -1 | -1 |
-1.5 | -2 | -1 | -1 | -1 |
-1.6 | -2 | -1 | -2 | -1 |
這些函數涵蓋了處理數字小數部分的所有可能方法。但是如果我們想將數字四捨五入到小數點後n-th
位呢?
例如,我們有1.2345
並希望將其四捨五入到 2 位數字,只得到1.23
。
有兩種方法可以做到這一點:
乘法和除法。
例如,要將數字四捨五入到小數點後第二位,我們可以將數字乘以100
,呼叫舍入函數,然後將其除回去。
令num = 1.23456; 警報( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
toFixed(n) 方法將數字四捨五入為小數點後的n
位,並傳回結果的字串表示形式。
令num = 12.34; 警報(num.toFixed(1)); //“12.3”
這會向上或向下舍入到最接近的值,類似於Math.round
:
令num = 12.36; 警報(num.toFixed(1)); //“12.4”
請注意, toFixed
的結果是一個字串。如果小數部分短於要求,則在末尾附加零:
令num = 12.34; 警報(num.toFixed(5)); //“12.34000”,添加零以恰好組成 5 位數字
我們可以使用一元加號或Number()
呼叫將其轉換為數字,例如編寫+num.toFixed(5)
。
在內部,數字以 64 位元格式 IEEE-754 表示,因此正好有 64 位元來儲存數字:其中 52 位元用於儲存數字,其中 11 位元用於儲存小數點位置,1 位元用於儲存數字。
如果一個數字非常大,它可能會溢出 64 位元儲存並成為一個特殊的數值Infinity
:
警報(1e500); // 無窮大
精度的損失可能不太明顯,但經常發生。
考慮這個(錯誤的!)相等性測試:
警報(0.1 + 0.2 == 0.3); // 錯誤的
沒錯,如果我們檢查0.1
和0.2
之和是否為0.3
,我們會得到false
。
奇怪的!如果不是0.3
那又是什麼呢?
警報(0.1 + 0.2); // 0.30000000000000004
哎喲!想像一下,您正在創建一個電子購物網站,訪客將$0.10
和$0.20
商品放入購物車。訂單總額將為$0.30000000000000004
。這會讓任何人感到驚訝。
但為什麼會出現這種情況呢?
數字以二進位形式儲存在記憶體中,即一系列位元(1 和 0)。但是像0.1
、 0.2
這樣的分數在十進制數字系統中看起來很簡單,但實際上是二進位形式的無限分數。
警報(0.1.toString(2)); // 0.0001100110011001100110011001100110011001100110011001101 警報(0.2.toString(2)); // 0.001100110011001100110011001100110011001100110011001101 警報((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 透過四捨五入到最接近的可能數字來解決這個問題。這些舍入規則通常不允許我們看到“微小的精度損失”,但它確實存在。
我們可以看到它的實際效果:
警報(0.1.toFixed(20)); // 0.10000000000000000555
當我們將兩個數字相加時,它們的「精度損失」就會相加。
這就是為什麼0.1 + 0.2
不完全是0.3
。
不僅僅是 JavaScript
許多其他程式語言也存在同樣的問題。
PHP、Java、C、Perl 和 Ruby 給出完全相同的結果,因為它們是基於相同的數字格式。
我們可以解決這個問題嗎?當然,最可靠的方法是藉助 toFixed(n) 方法來捨入結果:
設總和 = 0.1 + 0.2; 警報( sum.toFixed(2) ); //“0.30”
請注意, toFixed
始終傳回一個字串。它確保小數點後有 2 位數字。如果我們有網上購物並且需要出示$0.30
這實際上很方便。對於其他情況,我們可以使用一元加將其強制轉換為數字:
設總和 = 0.1 + 0.2; 警報( +sum.toFixed(2) ); // 0.3
我們也可以暫時將這些數字乘以 100(或更大的數字),將它們轉換為整數,進行數學運算,然後再除以。然後,當我們用整數進行數學運算時,誤差會有所減少,但我們仍然可以透過除法得到它:
警報((0.1 * 10 + 0.2 * 10)/ 10 ); // 0.3 警報((0.28 * 100 + 0.14 * 100)/ 100); // 0.4200000000000001
因此,乘法/除法方法可以減少誤差,但不能完全消除誤差。
有時我們可以嘗試迴避分數。就像我們正在與一家商店打交道一樣,我們可以用美分而不是美元來儲存價格。但如果我們要應用 30% 的折扣呢?實際上,完全規避分數幾乎是不可能的。需要時只需將它們圓形即可切掉“尾巴”。
有趣的事情
嘗試運行這個:
// 你好!我是一個自增數字! 警報(9999999999999999); // 顯示 10000000000000000
這也存在同樣的問題:精度損失。數字有 64 位,其中 52 位可用於儲存數字,但這還不夠。所以最低有效數字就消失了。
JavaScript 不會在此類事件中觸發錯誤。它會盡力將數字調整為所需的格式,但不幸的是,這種格式不夠大。
兩個零
數字內部表示的另一個有趣的結果是存在兩個零: 0
和-0
。
這是因為符號由單位元表示,因此可以為包括零在內的任何數字設定或不設定它。
在大多數情況下,這種區別是不明顯的,因為運算符適合將它們視為相同的。
還記得這兩個特殊的數值嗎?
Infinity
(和-Infinity
)是一個特殊的數值,大於(小於)任何值。
NaN
代表錯誤。
它們屬於number
類型,但不是“普通”數字,因此有特殊的函數來檢查它們:
isNaN(value)
將其參數轉換為數字,然後測試它是否為NaN
:
警報( isNaN(NaN) ); // 真的 警報(isNaN(“str”)); // 真的
但我們需要這個功能嗎?我們不能只使用比較=== NaN
嗎?不幸的是沒有。 NaN
值是唯一的,因為它不等於任何值,包括它本身:
警報(NaN===NaN); // 錯誤的
isFinite(value)
將其參數轉換為數字,如果它是常規數字而不是NaN/Infinity/-Infinity
則傳回true
:
警報(isFinite(“15”)); // 真的 警報(isFinite(“str”)); // false,因為有一個特殊值:NaN 警報( isFinite(無限大) ); // false,因為有一個特殊值:Infinity
有時isFinite
用於驗證字串值是否是常規數字:
let num = +prompt("請輸入一個數字", ''); // 除非您輸入 Infinity、-Infinity 或不是數字,否則將為 true 警報( isFinite(num) );
請注意,在所有數字函數(包括isFinite
中,空字串或僅包含空格的字串均視為0
。
Number.isNaN
和Number.isFinite
Number.isNaN 和 Number.isFinite 方法是isNaN
和isFinite
函數的更「嚴格」版本。他們不會將參數自動轉換為數字,而是檢查它是否屬於number
類型。
如果參數屬於number
類型且為NaN
,則Number.isNaN(value)
傳回true
。在任何其他情況下,它都會傳回false
。
警報( Number.isNaN(NaN) ); // 真的 Alert( Number.isNaN("str" / 2) ); // 真的 // 注意區別: 警報(Number.isNaN(“str”)); // false,因為「str」屬於字串類型,而不是數字類型 警報(isNaN(“str”)); // true,因為 isNaN 將字串「str」轉換為數字並得到 NaN 作為此轉換的結果
如果參數屬於number
類型且不是NaN/Infinity/-Infinity
Number.isFinite(value)
回傳true
。在任何其他情況下,它都會傳回false
。
警報( Number.isFinite(123) ); // 真的 警報( Number.isFinite(Infinity) ); // 錯誤的 警報( Number.isFinite(2 / 0) ); // 錯誤的 // 注意區別: 警報(Number.isFinite(“123”)); // false,因為「123」屬於字串類型,而不是數字類型 警報(isFinite(“123”)); // true,因為 isFinite 會將字串「123」轉換為數字 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)。
使用加號+
或Number()
進行數字轉換是嚴格的。如果一個值不完全是一個數字,則會失敗:
警報(+“100px”); // 南
唯一的例外是字串開頭或結尾的空格,因為它們會被忽略。
但在現實生活中,我們經常使用單位值,例如 CSS 中的"100px"
或"12pt"
。同樣在許多國家/地區,貨幣符號位於金額之後,因此我們有"19€"
,並且希望從中提取數值。
這就是parseInt
和parseFloat
的用途。
他們從字串中「讀取」數字,直到無法讀取為止。如果發生錯誤,則傳回收集到的數字。函數parseInt
傳回一個整數,而parseFloat
將傳回一個浮點數:
警報(parseInt('100px')); // 100 警報(parseFloat('12.5em')); // 12.5 警報(parseInt('12.3')); // 12、只回傳整數部分 警報(parseFloat('12.3.4')); // 12.3、第二點停止讀取
在某些情況下, parseInt/parseFloat
將傳回NaN
。當無法讀取數字時會發生這種情況:
警報(parseInt('a123')); // NaN,第一個符號停止進程
parseInt(str, radix)
的第二個參數
parseInt()
函數有一個可選的第二個參數。它指定了數字系統的基數,因此parseInt
還可以解析十六進位數字、二進位數字等字串:
警報( parseInt('0xff', 16) ); // 255 警報(parseInt('ff',16)); // 255,不含 0x 也可以 警報( parseInt('2n9c', 36) ); // 123456
JavaScript 有一個內建的 Math 對象,其中包含一個小型數學函數和常數庫。
舉幾個例子:
Math.random()
傳回 0 到 1 之間的隨機數(不包括 1)。
警報(Math.random()); // 0.1234567894322 警報(Math.random()); // 0.5435252343232 警報(Math.random()); // ...(任何隨機數字)
Math.max(a, b, c...)
和Math.min(a, b, c...)
傳回任意數量的參數中的最大值和最小值。
警報( Math.max(3, 5, -10, 0, 1) ); // 5 警報( Math.min(1, 2) ); // 1
Math.pow(n, power)
傳回n
給定次方。
警報( Math.pow(2, 10) ); // 2 的 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 物件。圖書館很小,但能滿足基本需求。
重要性:5
建立一個腳本,提示訪客輸入兩個數字,然後顯示它們的總和。
運行演示
PS 有一個關於類型的陷阱。
let a = +prompt("第一個數字?", ""); let b = +prompt("第二個數字?", ""); 警報(a + b);
請注意prompt
前的一元加號+
。它立即將值轉換為數字。
否則, a
和b
將是字串,它們的總和將是它們的串聯,即: "1" + "2" = "12"
。
重要性:4
根據文檔Math.round
和toFixed
都四捨五入到最接近的數字: 0..4
領先向下,而5..9
領先向上。
例如:
警報(1.35.toFixed(1)); // 1.4
在下面的類似範例中,為什麼6.35
四捨五入為6.3
,而不是6.4
?
警報(6.35.toFixed(1)); // 6.3
如何正確舍入6.35
?
在內部,小數6.35
是一個無限的二進位。像往常一樣,在這種情況下,它的儲存精度會有所損失。
讓我們來看看:
警報(6.35.toFixed(20)); // 6.34999999999999964473
精度損失會導致數字的增加和減少。在這種特殊情況下,數字會稍微減少一點,這就是它向下舍入的原因。
1.35
是多少?
警報(1.35.toFixed(20)); // 1.35000000000000008882
這裡,精度損失使數字稍大一些,因此四捨五入。
如果我們希望以正確的方式四捨五入,我們該如何解決6.35
的問題?
我們應該在四捨五入之前使其更接近整數:
警報((6.35 * 10).toFixed(20)); // 63.500000000000000000000
請注意, 63.5
根本沒有精度損失。這是因為小數部分0.5
其實是1/2
。除以2
的冪的分數在二進位系統中精確表示,現在我們可以將其捨入:
警報( Math.round(6.35 * 10) / 10 ); // 6.35 -> 63.5 -> 64(四捨五入) -> 6.4
重要性:5
建立一個函數readNumber
,該函數會提示輸入數字,直到訪客輸入有效的數值。
結果值必須以數字形式傳回。
訪客也可以透過輸入空白行或按「取消」來停止該過程。在這種情況下,函數應傳回null
。
運行演示
打開一個包含測試的沙箱。
函數 readNumber() { 讓數字; 做 { num =提示("請輸入數字?", 0); } while ( !isFinite(num) ); if (num === null || num === '') 傳回 null; 返回+數字; } Alert(`讀取:${readNumber()}`);
解決方案有點複雜,可能是因為我們需要處理null
/空行。
所以我們實際上接受輸入,直到它是一個“常規數字”。 null
(取消)和空白行也符合該條件,因為在數字形式中它們是0
。
停止後,我們需要特殊對待null
和空白行(返回null
),因為將它們轉換為數字將傳回0
。
在沙箱中開啟包含測試的解決方案。
重要性:4
這個循環是無限的。它永遠不會結束。為什麼?
讓我= 0; 而(我!= 10){ 我+= 0.2; }
那是因為i
永遠不會等於10
。
運行它來查看i
的實際值:
讓我= 0; 而(我<11){ 我+= 0.2; if (i > 9.8 && i < 10.2) 警報( i ); }
它們都不完全是10
。
發生這種情況是因為添加像0.2
這樣的分數時會造成精度損失。
結論:使用小數時逃避相等檢查。
重要性:2
內建函數Math.random()
建立一個從0
到1
隨機值(不包括1
)。
寫函數random(min, max)
產生從min
到max
(不包括max
)的隨機浮點數。
其工作範例:
警報(隨機(1, 5)); // 1.2345623452 警報(隨機(1, 5)); // 3.7894332423 警報(隨機(1, 5)); // 4.3435234525
我們需要將區間 0…1 中的所有值「映射」為從min
到max
值。
這可以分兩個階段完成:
如果我們將 0…1 中的隨機數乘以max-min
,則可能值的區間會增加0..1
到0..max-min
。
現在,如果我們加入min
,則可能的間隔將變為從min
到max
。
功能:
函數隨機(最小值,最大值){ 傳回最小值 + Math.random() * (最大值 - 最小值); } 警報(隨機(1, 5)); 警報(隨機(1, 5)); 警報(隨機(1, 5));
重要性:2
建立一個函數randomInteger(min, max)
,產生一個從min
到max
的隨機整數,包括min
和max
作為可能值。
區間min..max
中的任何數字必須以相同的機率出現。
其工作範例:
警報(隨機整數(1, 5)); // 1 警報(隨機整數(1, 5)); // 3 警報(隨機整數(1, 5)); // 5
您可以使用上一個任務的解決方案作為基礎。
最簡單但錯誤的解決方案是產生一個從min
到max
值並將其舍入:
函數隨機整數(最小值,最大值){ 讓 rand = min + Math.random() * (max - min); 返回 Math.round(rand); } 警報(隨機整數(1, 3));
該函數有效,但不正確。獲得邊緣值min
和max
機率比其他值小兩倍。
如果您多次運行上面的範例,您很容易會發現2
出現的頻率最高。
發生這種情況是因為Math.round()
從區間1..3
中取得隨機數並對它們進行舍入,如下所示:
從 1 ... 到 1.4999999999 的值變成 1 從 1.5 ... 到 2.4999999999 的值變為 2 從 2.5 ... 到 2.9999999999 的值變成 3
現在我們可以清楚地看到1
得到的值比2
少兩倍。與3
相同。
該任務有許多正確的解決方案。其中之一是調整區間邊界。為了確保相同的間隔,我們可以產生0.5 to 3.5
之間的值,從而將所需的機率加到邊緣:
函數隨機整數(最小值,最大值){ // 現在蘭特是從 (min-0.5) 到 (max+0.5) 讓 rand = min - 0.5 + Math.random() * (max - min + 1); 返回 Math.round(rand); } 警報(隨機整數(1, 3));
另一種方法是使用Math.floor
來取得從min
到max+1
隨機數:
函數隨機整數(最小值,最大值){ // 這裡 rand 是從 min 到 (max+1) 讓 rand = min + Math.random() * (max + 1 - min); 返回 Math.floor(rand); } 警報(隨機整數(1, 3));
現在所有間隔都以這種方式映射:
從 1 ... 到 1.9999999999 的值變成 1 從 2 ... 到 2.9999999999 的值變成 2 從 3 ... 到 3.9999999999 的值變成 3
所有區間的長度相同,使得最終分佈均勻。