_
) [ES2021]+
) 和負號 (-
)++
) 和遞減 (--
)NaN
Infinity
b32()
:以二進制記號顯示未簽署的 32 位元整數Number
的靜態屬性Number
的靜態方法Number.prototype
的方法JavaScript 有兩種數值
本章涵蓋數字。大整數會在 本書稍後 進行說明。
在 JavaScript 中,類型 number
用於整數和浮點數
98
123.45
但是,所有數字都是 雙精度,根據 浮點數算術 IEEE 標準 (IEEE 754) 實作的 64 位元浮點數。
整數只是沒有小數部分的浮點數
> 98 === 98.0true
請注意,在底層,大多數 JavaScript 引擎通常能夠使用真正的整數,並享有所有相關效能和儲存大小的優點。
讓我們來檢視數字的文字。
幾個 整數文字 讓我們可以用各種進位表示整數
// Binary (base 2)
.equal(0b11, 3); // ES6
assert
// Octal (base 8)
.equal(0o10, 8); // ES6
assert
// Decimal (base 10)
.equal(35, 35);
assert
// Hexadecimal (base 16)
.equal(0xE7, 231); assert
浮點數只能用 10 進位表示。
小數
> 35.035
次方:eN
表示 ×10N
> 3e2300
> 3e-20.03
> 0.3e230
存取整數文字的屬性會造成一個陷阱:如果整數文字緊接在一個點之後,則該點會被解釋為小數點
7.toString(); // syntax error
有四種方法可以解決這個陷阱
7.0.toString()
7).toString()
(7..toString()
7 .toString() // space before dot
_
) [ES2021]將數字分組以使長數字更易於閱讀的傳統由來已久。例如
自 ES2021 以來,我們可以在數字文字中使用底線作為分隔符號
const inhabitantsOfLondon = 1_335_000;
const distanceEarthSunInKm = 149_600_000;
在其他進位中,分組也很重要
const fileSystemPermission = 0b111_111_000;
const bytes = 0b1111_10101011_11110000_00001101;
const words = 0xFAB_F00D;
我們也可以在小數和小數中使用分隔符號
const massOfElectronInKg = 9.109_383_56e-31;
const trillionInShortScale = 1e1_2;
分隔符號的位置受到兩種方式的限制
我們只能在兩個數字之間放置底線。因此,以下所有數字文字都是不合法的
3_.141
3._141
1_e12
1e_12
// valid variable name!
_1464301 1464301_
0_b111111000
0b_111111000
我們不能連續使用多個底線
123__456 // two underscores – not allowed
這些限制背後的動機是保持解析的簡潔,並避免奇怪的邊緣情況。
用於解析數字的以下函式不支援分隔符號
Number()
Number.parseInt()
Number.parseFloat()
例如
> Number('123_456')NaN
> Number.parseInt('123_456')123
理由是數字分隔符號是寫給程式碼看的。其他類型的輸入應該以不同的方式處理。
表 5 列出了 JavaScript 的二元算術運算子。
運算子 | 名稱 | 範例 | |
---|---|---|---|
n + m |
加法 | ES1 | 3 + 4 → 7 |
n - m |
減法 | ES1 | 9 - 1 → 8 |
n * m |
乘法 | ES1 | 3 * 2.25 → 6.75 |
n / m |
除法 | ES1 | 5.625 / 5 → 1.125 |
n % m |
取餘數 | ES1 | 8 % 5 → 3 |
-8 % 5 → -3 |
|||
n ** m |
指數運算 | ES2016 | 4 ** 2 → 16 |
%
是取餘數運算子%
是取餘數運算子,不是模數運算子。它的結果具有第一個運算元的符號
> 5 % 32
> -5 % 3-2
有關餘數和模數之間差異的更多資訊,請參閱 2ality 上的部落格文章 “餘數運算子與模數運算子(含 JavaScript 程式碼)”。
+
) 和負號 (-
)表 6 總結了兩個運算子一元加號 (+
) 和負號 (-
)。
運算子 | 名稱 | 範例 | |
---|---|---|---|
+n |
一元加號 | ES1 | +(-7) → -7 |
-n |
一元負號 | ES1 | -(-7) → 7 |
這兩個運算子都會強制將它們的運算元轉換為數字
> +'5'5
> +'-12'-12
> -'9'-9
因此,一元加號讓我們可以將任意值轉換為數字。
++
) 和遞減 (--
)遞增運算子 ++
有前置版本和後置版本。在這兩個版本中,它都會對其運算元進行破壞性加一。因此,它的運算元必須是可以變更的儲存位置。
遞減運算子 --
的運作方式相同,但會從其運算元中減一。接下來的兩個範例說明了前置版本和後置版本之間的差異。
表 7 總結了遞增運算子和遞減運算子。
運算子 | 名稱 | 範例 | |
---|---|---|---|
v++ |
遞增 | ES1 | let v=0; [v++, v] → [0, 1] |
++v |
遞增 | ES1 | let v=0; [++v, v] → [1, 1] |
v-- |
遞減 | ES1 | let v=1; [v--, v] → [1, 0] |
--v |
遞減 | ES1 | let v=1; [--v, v] → [0, 0] |
接下來,我們將查看這些運算子在使用中的範例。
前置 ++
和前置 --
會變更它們的運算元,然後傳回它們。
let foo = 3;
.equal(++foo, 4);
assert.equal(foo, 4);
assert
let bar = 3;
.equal(--bar, 2);
assert.equal(bar, 2); assert
後置 ++
和後置 --
會傳回它們的運算元,然後變更它們。
let foo = 3;
.equal(foo++, 3);
assert.equal(foo, 4);
assert
let bar = 3;
.equal(bar--, 3);
assert.equal(bar, 2); assert
我們也可以將這些運算子套用至屬性值
const obj = { a: 1 };
++obj.a;
.equal(obj.a, 2); assert
以及陣列元素
const arr = [ 4 ];
0]++;
arr[.deepEqual(arr, [5]); assert
練習:數字運算子
exercises/numbers-math/is_odd_test.mjs
以下有 3 種將值轉換為數字的方法
Number(value)
+value
parseFloat(value)
(避免使用;與其他兩個不同!)建議:使用說明性的 Number()
。表格 8 摘要了它的運作方式。
x |
Number(x) |
---|---|
未定義 |
NaN |
null |
0 |
布林值 | false → 0 ,true → 1 |
數字 | x (無變更) |
BigInt | -1n → -1 ,1n → 1 ,依此類推。 |
字串 | '' → 0 |
其他 → 已剖析的數字,忽略前導/尾隨空白 |
|
符號 | 擲回 TypeError |
物件 | 可組態(例如透過 .valueOf() ) |
範例
.equal(Number(123.45), 123.45);
assert
.equal(Number(''), 0);
assert.equal(Number('\n 123.45 \t'), 123.45);
assert.equal(Number('xyz'), NaN);
assert
.equal(Number(-123n), -123); assert
物件轉換為數字的方式可以設定組態,例如覆寫 .valueOf()
> Number({ valueOf() { return 123 } })123
練習:轉換為數字
exercises/numbers-math/parse_number_test.mjs
發生錯誤時會傳回兩個數字值
NaN
無限大
NaN
NaN
是「非數字」的縮寫。諷刺的是,JavaScript 認為它是一個數字
> typeof NaN'number'
什麼時候會傳回 NaN
?
如果無法剖析數字,就會傳回 NaN
> Number('$$$')NaN
> Number(undefined)NaN
如果無法執行運算,就會傳回 NaN
> Math.log(-1)NaN
> Math.sqrt(-1)NaN
如果運算元或引數是 NaN
(傳播錯誤),就會傳回 NaN
> NaN - 3NaN
> 7 ** NaNNaN
NaN
NaN
是唯一一個與自身不相等的 JavaScript 值
const n = NaN;
.equal(n === n, false); assert
以下有幾種檢查值 x
是否為 NaN
的方法
const x = NaN;
.equal(Number.isNaN(x), true); // preferred
assert.equal(Object.is(x, NaN), true);
assert.equal(x !== x, true); assert
在最後一行,我們使用比較怪癖來偵測 NaN
。
NaN
有些陣列方法找不到 NaN
> [NaN].indexOf(NaN)-1
有些可以
> [NaN].includes(NaN)true
> [NaN].findIndex(x => Number.isNaN(x))0
> [NaN].find(x => Number.isNaN(x))NaN
唉,沒有簡單的經驗法則。我們必須檢查每個方法如何處理 NaN
。
Infinity
什麼時候會傳回錯誤值 Infinity
?
如果數字太大,就會傳回無限大
> Math.pow(2, 1023)8.98846567431158e+307
> Math.pow(2, 1024)Infinity
如果除以零,就會傳回無限大
> 5 / 0Infinity
> -5 / 0-Infinity
Infinity
作為預設值Infinity
大於所有其他數字(NaN
除外),使其成為一個良好的預設值
function findMinimum(numbers) {
let min = Infinity;
for (const n of numbers) {
if (n < min) min = n;
}return min;
}
.equal(findMinimum([5, -1, 2]), -1);
assert.equal(findMinimum([]), Infinity); assert
Infinity
以下兩種常見方法可檢查值 x
是否為 Infinity
const x = Infinity;
.equal(x === Infinity, true);
assert.equal(Number.isFinite(x), false); assert
練習:比較數字
exercises/numbers-math/find_max_test.mjs
在內部,JavaScript 浮點數以 2 為底數表示(根據 IEEE 754 標準)。這表示小數(10 為底數)無法永遠精確表示
> 0.1 + 0.20.30000000000000004
> 1.3 * 33.9000000000000004
> 1.4 * 100000000000000139999999999999.98
因此,在 JavaScript 中執行算術運算時,我們需要考慮捨入誤差。
請繼續閱讀以了解此現象的說明。
測驗:基礎
請參閱 測驗應用程式。
本章節的其餘部分都是進階內容。
在 JavaScript 中,數字運算並不總是會產生正確的結果,例如
> 0.1 + 0.20.30000000000000004
要了解原因,我們需要探討 JavaScript 在內部如何表示浮點數。它使用三個整數來執行此操作,總共佔用 64 位元的儲存空間(雙精度)
元件 | 大小 | 整數範圍 |
---|---|---|
符號 | 1 位元 | [0, 1] |
小數 | 52 位元 | [0, 252−1] |
指數 | 11 位元 | [−1023, 1024] |
由這些整數表示的浮點數計算如下
(–1)符號 × 0b1.小數 × 2指數
此表示法無法編碼零,因為其第二個元件(涉及小數)總是有一個前導 1。因此,零透過特殊指數 −1023 和小數 0 編碼。
為了讓後續討論更為容易,我們簡化先前的表示法
新的表示法如下
尾數 × 10指數
讓我們嘗試使用此表示法表示幾個浮點數。
對於整數 −123,我們主要需要尾數
> -123 * (10 ** 0)-123
對於數字 1.5,我們想像在尾數後有一個小數點。我們使用一個負指數將該小數點向左移動一位數
> 15 * (10 ** -1)1.5
對於數字 0.25,我們將小數點向左移動兩位數
> 25 * (10 ** -2)0.25
具有負指數的表示法也可以寫成分母中具有正指數的分數
> 15 * (10 ** -1) === 15 / (10 ** 1)true
> 25 * (10 ** -2) === 25 / (10 ** 2)true
這些分數有助於我們了解為什麼有些數字無法透過我們的編碼表示
1/10
可以表示。它已經具有所需的格式:分母中的 10 的次方。
1/2
可以表示為 5/10
。我們乘分子和分母以 5,將分母中的 2 轉換為 10 的冪。
1/4
可以表示為 25/100
。我們乘分子和分母以 25,將分母中的 4 轉換為 10 的冪。
1/3
無法表示。沒有辦法將分母轉換為 10 的冪。(10 的質因數為 2 和 5。因此,任何只有這些質因數的分母都可以透過乘以足夠的 2 和 5,將分子和分母轉換為 10 的冪。如果分母有不同的質因數,那麼我們就無能為力了。)
為了結束我們的遊覽,我們切換回 2 進位
0.5 = 1/2
可以用 2 進位表示,因為分母已經是 2 的冪。0.25 = 1/4
可以用 2 進位表示,因為分母已經是 2 的冪。0.1 = 1/10
無法表示,因為分母無法轉換為 2 的冪。0.2 = 2/10
無法表示,因為分母無法轉換為 2 的冪。現在我們可以看到為什麼 0.1 + 0.2
無法產生正確的結果:在內部,兩個運算元都無法精確表示。
精確計算十進位分數的唯一方法是內部切換到 10 進位。對於許多程式語言,2 進位是預設值,10 進位是選項。例如,Java 有類別 BigDecimal
,Python 有模組 decimal
。有計畫在 JavaScript 中新增類似的東西:ECMAScript 提議「Decimal」。
整數是沒有小數部分的正常(浮點)數字
> 1 === 1.0true
> Number.isInteger(1.0)true
在本節中,我們將探討一些使用這些偽整數的工具。JavaScript 也支援 大整數,它們是真正的整數。
將數字轉換為整數的建議方法是使用 Math
物件的其中一種捨入方法
Math.floor(n)
:傳回最大的整數 i
≤ n
> Math.floor(2.1)2
> Math.floor(2.9)2
Math.ceil(n)
:傳回最小的整數 i
≥ n
> Math.ceil(2.1)3
> Math.ceil(2.9)3
Math.round(n)
:傳回與 n
「最接近」的整數,其中 __.5
會向上捨入,例如
> Math.round(2.4)2
> Math.round(2.5)3
Math.trunc(n)
:移除 n
的任何小數部分(小數點後),因此將其轉換為整數。
> Math.trunc(2.1)2
> Math.trunc(2.9)2
有關捨入的更多資訊,請參閱 §17.3「捨入」。
以下是 JavaScript 中整數範圍的重要範圍
>>>
):無符號,[0, 232)這是 JavaScript 中安全的整數範圍(53 位元加符號)
[–(253)+1, 253–1]
如果整數由一個 JavaScript 數字精確表示,則該整數為安全。由於 JavaScript 數字編碼為一個分數乘以 2 的某次方,因此也可以表示較高的整數,但它們之間會有間隔。
例如(18014398509481984 是 254)
> 1801439850948198418014398509481984
> 1801439850948198518014398509481984
> 1801439850948198618014398509481984
> 1801439850948198718014398509481988
Number
的下列屬性有助於判斷整數是否安全
.equal(Number.MAX_SAFE_INTEGER, (2 ** 53) - 1);
assert.equal(Number.MIN_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER);
assert
.equal(Number.isSafeInteger(5), true);
assert.equal(Number.isSafeInteger('5'), false);
assert.equal(Number.isSafeInteger(5.1), false);
assert.equal(Number.isSafeInteger(Number.MAX_SAFE_INTEGER), true);
assert.equal(Number.isSafeInteger(Number.MAX_SAFE_INTEGER+1), false); assert
練習:偵測安全整數
exercises/numbers-math/is_safe_integer_test.mjs
讓我們看看涉及不安全整數的運算。
以下結果不正確且不安全,即使其兩個運算元都是安全的
> 9007199254740990 + 39007199254740992
以下結果是安全的,但錯誤的。第一個運算元不安全;第二個運算元是安全的
> 9007199254740995 - 109007199254740986
因此,表達式 a op b
的結果僅在下列情況下才正確
isSafeInteger(a) && isSafeInteger(b) && isSafeInteger(a op b)
也就是說,運算元和結果都必須是安全的。
內部而言,JavaScript 的按位元運算子使用 32 位元整數。它們按照下列步驟產生結果
對於每個按位元運算子,本書會提到其運算元的類型和其結果。每個類型永遠都是下列兩個之一
類型 | 說明 | 大小 | 範圍 |
---|---|---|---|
Int32 | 有符號 32 位元整數 | 32 位元包含符號 | [−231, 231) |
Uint32 | 無符號 32 位元整數 | 32 位元 | [0, 232) |
考量先前提到的步驟,我建議假裝按位元運算子在內部使用無符號 32 位元整數(步驟「運算」),而 Int32 和 Uint32 只會影響 JavaScript 數字如何轉換為整數,以及從整數轉換為 JavaScript 數字(步驟「輸入」和「輸出」)。
在探索按位元運算子時,偶爾會需要將 JavaScript 數字顯示為二進位表示法的無符號 32 位元整數。這就是 b32()
的功能(實作稍後會說明)
.equal(
assertb32(-1),
'11111111111111111111111111111111');
.equal(
assertb32(1),
'00000000000000000000000000000001');
.equal(
assertb32(2 ** 31),
'10000000000000000000000000000000');
運算 | 名稱 | 類型簽章 | |
---|---|---|---|
~num |
按位元非,一補數 | Int32 → Int32 |
ES1 |
按位元非運算子(表 9)會反轉其運算元的每個二進位數字
> b32(~0b100)'11111111111111111111111111111011'
這個所謂的一補數類似於某些算術運算的負數。例如,將整數加上其一補數永遠等於 -1
> 4 + ~4-1
> -11 + ~-11-1
運算 | 名稱 | 類型簽章 | |
---|---|---|---|
num1 & num2 |
按位元與 | Int32 × Int32 → Int32 |
ES1 |
num1 ¦ num2 |
按位元或 | Int32 × Int32 → Int32 |
ES1 |
num1 ^ num2 |
按位元異或 | Int32 × Int32 → Int32 |
ES1 |
二進位按位元運算子(表 10)會結合其運算元的位元來產生其結果
> (0b1010 & 0b0011).toString(2).padStart(4, '0')'0010'
> (0b1010 | 0b0011).toString(2).padStart(4, '0')'1011'
> (0b1010 ^ 0b0011).toString(2).padStart(4, '0')'1001'
運算 | 名稱 | 類型簽章 | |
---|---|---|---|
num << count |
左位移 | Int32 × Uint32 → Int32 |
ES1 |
num >> count |
有符號右位移 | Int32 × Uint32 → Int32 |
ES1 |
num >>> count |
無符號右位移 | Uint32 × Uint32 → Uint32 |
ES1 |
位移運算子(表 11)會將二進位數字向左或向右移動
> (0b10 << 1).toString(2)'100'
>>
會保留最高位元,>>>
則不會
> b32(0b10000000000000000000000000000010 >> 1)'11000000000000000000000000000001'
> b32(0b10000000000000000000000000000010 >>> 1)'01000000000000000000000000000001'
b32()
:將無符號 32 位元整數顯示為二進位表示法我們現在已經使用 b32()
幾次了。下列程式碼是它的實作
/**
* Return a string representing n as a 32-bit unsigned integer,
* in binary notation.
*/
function b32(n) {
// >>> ensures highest bit isn’t interpreted as a sign
return (n >>> 0).toString(2).padStart(32, '0');
}.equal(
assertb32(6),
'00000000000000000000000000000110');
n >>> 0
表示我們將 n
向右位移 0 個位元。因此,原則上,>>>
運算子什麼都不做,但它仍會強制 n
轉換為無符號 32 位元整數
> 12 >>> 012
> -12 >>> 04294967284
> (2**32 + 1) >>> 01
JavaScript 有下列四個數字的全球函式
isFinite()
isNaN()
parseFloat()
parseInt()
然而,最好使用對應的 Number
方法(Number.isFinite()
等),它們有較少的陷阱。它們在 ES6 中引入,並在下面討論。
Number
的靜態屬性.EPSILON: number
[ES6]
1 與下一個可表示浮點數之間的差。一般來說,機器 epsilon 提供浮點運算中捨入誤差的上限。
.MAX_SAFE_INTEGER: number
[ES6]
JavaScript 可以明確表示的最大整數(253−1)。
.MAX_VALUE: number
[ES1]
最大的正有限 JavaScript 數字。
.MIN_SAFE_INTEGER: number
[ES6]
JavaScript 可以明確表示的最小的整數(−253+1)。
.MIN_VALUE: number
[ES1]
最小的正 JavaScript 數字。大約 5 × 10−324。
.NaN: number
[ES1]
與全域變數 NaN
相同。
.NEGATIVE_INFINITY: number
[ES1]
與 -Number.POSITIVE_INFINITY
相同。
.POSITIVE_INFINITY: number
[ES1]
與全域變數 Infinity
相同。
Number
的靜態方法.isFinite(num: number): boolean
[ES6]
如果 num
是實際數字(既不是 Infinity
也不是 -Infinity
也不是 NaN
),則傳回 true
。
> Number.isFinite(Infinity)false
> Number.isFinite(-Infinity)false
> Number.isFinite(NaN)false
> Number.isFinite(123)true
.isInteger(num: number): boolean
[ES6]
如果 num
是數字且沒有小數部分,則傳回 true
。
> Number.isInteger(-17)true
> Number.isInteger(33)true
> Number.isInteger(33.1)false
> Number.isInteger('33')false
> Number.isInteger(NaN)false
> Number.isInteger(Infinity)false
.isNaN(num: number): boolean
[ES6]
如果 num
是值 NaN
,則傳回 true
。
> Number.isNaN(NaN)true
> Number.isNaN(123)false
> Number.isNaN('abc')false
.isSafeInteger(num: number): boolean
[ES6]
如果 num
是數字且明確表示整數,則傳回 true
。
.parseFloat(str: string): number
[ES6]
強制將其參數轉換為字串並將其解析為浮點數。對於將字串轉換為數字,Number()
(忽略前導和尾隨空白)通常比 Number.parseFloat()
(忽略前導空白和非法尾隨字元,並且可能隱藏問題)更好。
> Number.parseFloat(' 123.4#')123.4
> Number(' 123.4#')NaN
.parseInt(str: string, radix=10): number
[ES6]
強制將其參數轉換為字串並將其解析為整數,忽略前導空白和非法尾隨字元。
> Number.parseInt(' 123#')123
參數 radix
指定要解析的數字的底數
> Number.parseInt('101', 2)5
> Number.parseInt('FF', 16)255
不要使用這個方法將數字轉換為整數:強制轉換為字串效率很低。而且在第一個非數字之前停止並不是移除數字小數部分的好演算法。以下是一個出錯的範例
> Number.parseInt(1e21, 10) // wrong1
最好使用 Math
的其中一個捨入函數將數字轉換為整數
> Math.trunc(1e21) // correct1e+21
Number.prototype
的方法(Number.prototype
儲存數字的方法。)
.toExponential(fractionDigits?: number): string
[ES3]
傳回一個使用指數符號表示數字的字串。使用 fractionDigits
,我們可以指定要顯示乘以指數的數字的位數(預設顯示必要的位數)。
範例:數字太小,無法透過 .toString()
取得正指數。
> 1234..toString()'1234'
> 1234..toExponential() // 3 fraction digits'1.234e+3'
> 1234..toExponential(5)'1.23400e+3'
> 1234..toExponential(1)'1.2e+3'
範例:小數不夠小,無法透過 .toString()
取得負指數。
> 0.003.toString()'0.003'
> 0.003.toExponential()'3e-3'
.toFixed(fractionDigits=0): string
[ES3]
傳回數字的無指數表示,四捨五入到 fractionDigits
位數。
> 0.00000012.toString() // with exponent'1.2e-7'
> 0.00000012.toFixed(10) // no exponent'0.0000001200'
> 0.00000012.toFixed()'0'
如果數字是 1021 或更大,即使 .toFixed()
也會使用指數
> (10 ** 21).toFixed()'1e+21'
.toPrecision(precision?: number): string
[ES3]
運作方式類似 .toString()
,但 precision
指定要顯示的位數。如果沒有 precision
,則使用 .toString()
。
> 1234..toPrecision(3) // requires exponential notation'1.23e+3'
> 1234..toPrecision(4)'1234'
> 1234..toPrecision(5)'1234.0'
> 1.234.toPrecision(3)'1.23'
.toString(radix=10): string
[ES1]
傳回數字的字串表示。
預設情況下,我們會得到一個底數為 10 的數字
> 123.456.toString()'123.456'
如果我們希望數字有不同的底數,我們可以使用 radix
指定
> 4..toString(2) // binary (base 2)'100'
> 4.5.toString(2)'100.1'
> 255..toString(16) // hexadecimal (base 16)'ff'
> 255.66796875.toString(16)'ff.ab'
> 1234567890..toString(36)'kf12oi'
Number.parseInt()
提供反向操作:它將包含整數(沒有小數!)數字的字串,以給定的底數轉換為數字。
> Number.parseInt('kf12oi', 36)1234567890
測驗:進階
請參閱 測驗應用程式。