JavaScript for impatient programmers (ES2022 版)
請支持這本書:購買捐款
(廣告,請不要阻擋。)

16 數字



JavaScript 有兩種數值

本章涵蓋數字。大整數會在 本書稍後 進行說明。

16.1 數字用於浮點數和整數

在 JavaScript 中,類型 number 用於整數和浮點數

98
123.45

但是,所有數字都是 雙精度,根據 浮點數算術 IEEE 標準 (IEEE 754) 實作的 64 位元浮點數。

整數只是沒有小數部分的浮點數

> 98 === 98.0
true

請注意,在底層,大多數 JavaScript 引擎通常能夠使用真正的整數,並享有所有相關效能和儲存大小的優點。

16.2 數字文字

讓我們來檢視數字的文字。

16.2.1 整數文字

幾個 整數文字 讓我們可以用各種進位表示整數

// Binary (base 2)
assert.equal(0b11, 3); // ES6

// Octal (base 8)
assert.equal(0o10, 8); // ES6

// Decimal (base 10)
assert.equal(35, 35);

// Hexadecimal (base 16)
assert.equal(0xE7, 231);

16.2.2 浮點數文字

浮點數只能用 10 進位表示。

小數

> 35.0
35

次方:eN 表示 ×10N

> 3e2
300
> 3e-2
0.03
> 0.3e2
30

16.2.3 語法陷阱:整數文字的屬性

存取整數文字的屬性會造成一個陷阱:如果整數文字緊接在一個點之後,則該點會被解釋為小數點

7.toString(); // syntax error

有四種方法可以解決這個陷阱

7.0.toString()
(7).toString()
7..toString()
7 .toString()  // space before dot

16.2.4 數字文字中分隔符號 (_) [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;
16.2.4.1 我們可以在哪裡放置分隔符號?

分隔符號的位置受到兩種方式的限制

這些限制背後的動機是保持解析的簡潔,並避免奇怪的邊緣情況。

16.2.4.2 解析有分隔符號的數字

用於解析數字的以下函式不支援分隔符號

例如

> Number('123_456')
NaN
> Number.parseInt('123_456')
123

理由是數字分隔符號是寫給程式碼看的。其他類型的輸入應該以不同的方式處理。

16.3 算術運算子

16.3.1 二元算術運算子

表 5 列出了 JavaScript 的二元算術運算子。

表 5:二元算術運算子。
運算子 名稱 範例
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
16.3.1.1 % 是取餘數運算子

% 是取餘數運算子,不是模數運算子。它的結果具有第一個運算元的符號

> 5 % 3
2
> -5 % 3
-2

有關餘數和模數之間差異的更多資訊,請參閱 2ality 上的部落格文章 “餘數運算子與模數運算子(含 JavaScript 程式碼)”

16.3.2 一元加號 (+) 和負號 (-)

表 6 總結了兩個運算子一元加號 (+) 和負號 (-)。

表 6:一元加號 (+) 和負號 (-) 運算子。
運算子 名稱 範例
+n 一元加號 ES1 +(-7) -7
-n 一元負號 ES1 -(-7) 7

這兩個運算子都會強制將它們的運算元轉換為數字

> +'5'
5
> +'-12'
-12
> -'9'
-9

因此,一元加號讓我們可以將任意值轉換為數字。

16.3.3 遞增 (++) 和遞減 (--)

遞增運算子 ++ 有前置版本和後置版本。在這兩個版本中,它都會對其運算元進行破壞性加一。因此,它的運算元必須是可以變更的儲存位置。

遞減運算子 -- 的運作方式相同,但會從其運算元中減一。接下來的兩個範例說明了前置版本和後置版本之間的差異。

表 7 總結了遞增運算子和遞減運算子。

表 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;
assert.equal(++foo, 4);
assert.equal(foo, 4);

let bar = 3;
assert.equal(--bar, 2);
assert.equal(bar, 2);

後置 ++ 和後置 -- 會傳回它們的運算元,然後變更它們。

let foo = 3;
assert.equal(foo++, 3);
assert.equal(foo, 4);

let bar = 3;
assert.equal(bar--, 3);
assert.equal(bar, 2);
16.3.3.1 運算元:不只是變數

我們也可以將這些運算子套用至屬性值

const obj = { a: 1 };
++obj.a;
assert.equal(obj.a, 2);

以及陣列元素

const arr = [ 4 ];
arr[0]++;
assert.deepEqual(arr, [5]);

  練習:數字運算子

exercises/numbers-math/is_odd_test.mjs

16.4 轉換為數字

以下有 3 種將值轉換為數字的方法

建議:使用說明性的 Number()。表格 8 摘要了它的運作方式。

表格 8:將值轉換為數字。
x Number(x)
未定義 NaN
null 0
布林值 false 0true 1
數字 x(無變更)
BigInt -1n -11n 1,依此類推。
字串 '' 0
其他 已剖析的數字,忽略前導/尾隨空白
符號 擲回 TypeError
物件 可組態(例如透過 .valueOf()

範例

assert.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);

物件轉換為數字的方式可以設定組態,例如覆寫 .valueOf()

> Number({ valueOf() { return 123 } })
123

  練習:轉換為數字

exercises/numbers-math/parse_number_test.mjs

16.5 錯誤值

發生錯誤時會傳回兩個數字值

16.5.1 錯誤值: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 - 3
NaN
> 7 ** NaN
NaN
16.5.1.1 檢查 NaN

NaN 是唯一一個與自身不相等的 JavaScript 值

const n = NaN;
assert.equal(n === n, false);

以下有幾種檢查值 x 是否為 NaN 的方法

const x = NaN;

assert.equal(Number.isNaN(x), true); // preferred
assert.equal(Object.is(x, NaN), true);
assert.equal(x !== x, true);

在最後一行,我們使用比較怪癖來偵測 NaN

16.5.1.2 在陣列中尋找 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

16.5.2 錯誤值:Infinity

什麼時候會傳回錯誤值 Infinity

如果數字太大,就會傳回無限大

> Math.pow(2, 1023)
8.98846567431158e+307
> Math.pow(2, 1024)
Infinity

如果除以零,就會傳回無限大

> 5 / 0
Infinity
> -5 / 0
-Infinity
16.5.2.1 Infinity 作為預設值

Infinity 大於所有其他數字(NaN 除外),使其成為一個良好的預設值

function findMinimum(numbers) {
  let min = Infinity;
  for (const n of numbers) {
    if (n < min) min = n;
  }
  return min;
}

assert.equal(findMinimum([5, -1, 2]), -1);
assert.equal(findMinimum([]), Infinity);
16.5.2.2 檢查 Infinity

以下兩種常見方法可檢查值 x 是否為 Infinity

const x = Infinity;

assert.equal(x === Infinity, true);
assert.equal(Number.isFinite(x), false);

  練習:比較數字

exercises/numbers-math/find_max_test.mjs

16.6 數字的精度:小心處理小數

在內部,JavaScript 浮點數以 2 為底數表示(根據 IEEE 754 標準)。這表示小數(10 為底數)無法永遠精確表示

> 0.1 + 0.2
0.30000000000000004
> 1.3 * 3
3.9000000000000004
> 1.4 * 100000000000000
139999999999999.98

因此,在 JavaScript 中執行算術運算時,我們需要考慮捨入誤差。

請繼續閱讀以了解此現象的說明。

  測驗:基礎

請參閱 測驗應用程式

16.7 (進階)

本章節的其餘部分都是進階內容。

16.8 背景:浮點精度

在 JavaScript 中,數字運算並不總是會產生正確的結果,例如

> 0.1 + 0.2
0.30000000000000004

要了解原因,我們需要探討 JavaScript 在內部如何表示浮點數。它使用三個整數來執行此操作,總共佔用 64 位元的儲存空間(雙精度)

元件 大小 整數範圍
符號 1 位元 [0, 1]
小數 52 位元 [0, 252−1]
指數 11 位元 [−1023, 1024]

由這些整數表示的浮點數計算如下

(–1)符號 × 0b1.小數 × 2指數

此表示法無法編碼零,因為其第二個元件(涉及小數)總是有一個前導 1。因此,零透過特殊指數 −1023 和小數 0 編碼。

16.8.1 浮點數的簡化表示法

為了讓後續討論更為容易,我們簡化先前的表示法

新的表示法如下

尾數 × 10指數

讓我們嘗試使用此表示法表示幾個浮點數。

具有負指數的表示法也可以寫成分母中具有正指數的分數

> 15 * (10 ** -1) === 15 / (10 ** 1)
true
> 25 * (10 ** -2) === 25 / (10 ** 2)
true

這些分數有助於我們了解為什麼有些數字無法透過我們的編碼表示

為了結束我們的遊覽,我們切換回 2 進位

現在我們可以看到為什麼 0.1 + 0.2 無法產生正確的結果:在內部,兩個運算元都無法精確表示。

精確計算十進位分數的唯一方法是內部切換到 10 進位。對於許多程式語言,2 進位是預設值,10 進位是選項。例如,Java 有類別 BigDecimal,Python 有模組 decimal。有計畫在 JavaScript 中新增類似的東西:ECMAScript 提議「Decimal」

16.9 JavaScript 中的整數

整數是沒有小數部分的正常(浮點)數字

> 1 === 1.0
true
> Number.isInteger(1.0)
true

在本節中,我們將探討一些使用這些偽整數的工具。JavaScript 也支援 大整數,它們是真正的整數。

16.9.1 轉換為整數

將數字轉換為整數的建議方法是使用 Math 物件的其中一種捨入方法

有關捨入的更多資訊,請參閱 §17.3「捨入」

16.9.2 JavaScript 中整數範圍

以下是 JavaScript 中整數範圍的重要範圍

16.9.3 安全整數

這是 JavaScript 中安全的整數範圍(53 位元加符號)

[–(253)+1, 253–1]

如果整數由一個 JavaScript 數字精確表示,則該整數為安全。由於 JavaScript 數字編碼為一個分數乘以 2 的某次方,因此也可以表示較高的整數,但它們之間會有間隔。

例如(18014398509481984 是 254

> 18014398509481984
18014398509481984
> 18014398509481985
18014398509481984
> 18014398509481986
18014398509481984
> 18014398509481987
18014398509481988

Number 的下列屬性有助於判斷整數是否安全

assert.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);

  練習:偵測安全整數

exercises/numbers-math/is_safe_integer_test.mjs

16.9.3.1 安全運算

讓我們看看涉及不安全整數的運算。

以下結果不正確且不安全,即使其兩個運算元都是安全的

> 9007199254740990 + 3
9007199254740992

以下結果是安全的,但錯誤的。第一個運算元不安全;第二個運算元是安全的

> 9007199254740995 - 10
9007199254740986

因此,表達式 a op b 的結果僅在下列情況下才正確

isSafeInteger(a) && isSafeInteger(b) && isSafeInteger(a op b)

也就是說,運算元和結果都必須是安全的。

16.10 按位元運算子

16.10.1 內部而言,按位元運算子使用 32 位元整數

內部而言,JavaScript 的按位元運算子使用 32 位元整數。它們按照下列步驟產生結果

16.10.1.1 運算元和結果的類型

對於每個按位元運算子,本書會提到其運算元的類型和其結果。每個類型永遠都是下列兩個之一

類型 說明 大小 範圍
Int32 有符號 32 位元整數 32 位元包含符號 [−231, 231)
Uint32 無符號 32 位元整數 32 位元 [0, 232)

考量先前提到的步驟,我建議假裝按位元運算子在內部使用無符號 32 位元整數(步驟「運算」),而 Int32 和 Uint32 只會影響 JavaScript 數字如何轉換為整數,以及從整數轉換為 JavaScript 數字(步驟「輸入」和「輸出」)。

16.10.1.2 將 JavaScript 數字顯示為無符號 32 位元整數

在探索按位元運算子時,偶爾會需要將 JavaScript 數字顯示為二進位表示法的無符號 32 位元整數。這就是 b32() 的功能(實作稍後會說明)

assert.equal(
  b32(-1),
  '11111111111111111111111111111111');
assert.equal(
  b32(1),
  '00000000000000000000000000000001');
assert.equal(
  b32(2 ** 31),
  '10000000000000000000000000000000');

16.10.2 按位元非

表格 9:按位元非運算子。
運算 名稱 類型簽章
~num 按位元非,一補數 Int32 Int32 ES1

按位元非運算子(表 9)會反轉其運算元的每個二進位數字

> b32(~0b100)
'11111111111111111111111111111011'

這個所謂的一補數類似於某些算術運算的負數。例如,將整數加上其一補數永遠等於 -1

> 4 + ~4
-1
> -11 + ~-11
-1

16.10.3 二進位按位元運算子

表格 10:二進位按位元運算子。
運算 名稱 類型簽章
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'

16.10.4 按位元位移運算子

表格 11:按位元位移運算子。
運算 名稱 類型簽章
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'

16.10.5 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');
}
assert.equal(
  b32(6),
  '00000000000000000000000000000110');

n >>> 0 表示我們將 n 向右位移 0 個位元。因此,原則上,>>> 運算子什麼都不做,但它仍會強制 n 轉換為無符號 32 位元整數

> 12 >>> 0
12
> -12 >>> 0
4294967284
> (2**32 + 1) >>> 0
1

16.11 快速參考:數字

16.11.1 數字的全球函式

JavaScript 有下列四個數字的全球函式

然而,最好使用對應的 Number 方法(Number.isFinite() 等),它們有較少的陷阱。它們在 ES6 中引入,並在下面討論。

16.11.2 Number 的靜態屬性

16.11.3 Number 的靜態方法

16.11.4 Number.prototype 的方法

Number.prototype 儲存數字的方法。)

16.11.5 來源

  測驗:進階

請參閱 測驗應用程式