在什麼情況下 a === a - 1 ?

語言: CN / TW / HK

從一道可(變)愛(態)的面試題說起。

上週,我們團隊小仙女同學考了我一道面試題,題目是:在什麼情況下,a === a - 1

我也不知道這道題具體來源是誰,但是作為一位沉浸於前端多年的老江湖,這種題目自然是難不倒我的。

當然這道題其實並不難,很多同學應該第一感就能說出一個答案,但是這道題有幾個答案呢?你們能不能說出更多答案,甚至是無窮多個答案來?

思考 10 秒鐘再往下看——

第一個答案自然是 Infinity,或者說,擴充套件一下,應該是正負 Infinity。

👉🏻 知識點: 在 JavaScript 裡,Infinity是一個 Number 型別的字面量,表示無窮大。當一個 Number 型別的值,在運算過程中超過了所能表示的最大值,就會得到無窮大。

比如,如果我們將一個不為 0 的正數除以 0,得到的結果就是無窮大。

console.log(100 / 0); // Infinity

複製程式碼

對應的,負數有負無窮大。

console.log(-100 / 0); // -Infinity

複製程式碼

如果我們數值運算的值,超過了 Number 允許表示的範圍,也是會得到 Infinity。

console.log(1e1000); // Infinity

複製程式碼

在 JavaScript 裡,Number.POSITIVE_INFINITYNumber.NEGATIVE_INFINITY兩個常量的值,對應正負 Infinity。

Number.isFinite()可以判斷一個數是否是有窮的,Number.isFinite(n),當 n 是 Number 型別時,只有它是正負 Infinity 或 NaN 時,返回 false,其他情況下返回 true。

任何一個有窮的數和 Infinity 的加減運算的結果都是 Infinity,而Infinity === Infinity,所以:

let a = Infinity;

console.log(a === a - 1); // true

let b = -Infinity;

console.log(b === b - 1);  // true

複製程式碼

這樣我們就得到了兩個答案。

💡 但是,要注意,Infinity 運算的結果並不總是 Infinity,比如我們看下面幾種運算:

console.log(Infinity + Infinity); // Infinity
console.log(Infinity - Infinity); // NaN
console.log(Infinity * Infinity); // Infinity
console.log(Infinity / Infinity); // NaN
console.log(Infinity * 0); // NaN

複製程式碼

結論是,Infinity 運算也有可能得到 NaN,所以需要小心,例如我們的一個計算表示式中,有兩個值相乘,一個值有可能很大,另一個值有可能為 0 時,就需要小心,如果那個很大的值得到 Infinity,另一個值恰好為 0 時,整個表示式的值可能是 NaN,這會造成一些 bug。

const result = a + b * c + d;

// 如果 b 是 Infinity 而 c 是 0,整個表示式的結果就有可能是 NaN

複製程式碼

好了,以上是我們的第一個答案:正負 Infinity。

接下來,我們看另一個(另一些)答案。

我們給 a 一個比較大的數值,比如 1e45:

let a = 1e45;
console.log(a);	// 1e+45
console.log(a === a - 1); // true

複製程式碼

有些同學一看,誒,這也行?

這個不但可以,你隨便找兩個比較大的數,應該都是可以的:

let a = 6.22e23;
console.log(a === a - 1); // true

複製程式碼

那這又是怎麼回事呢?

👉🏻 知識點:在 JavaScript 裡,整數可以被精確表示的範圍是從-2 ** 53 + 12 ** 53 - 1,即-90071992547409919007199254740991。超過這個數值的整數,都不能被精確表示。

常量Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER分別對應9007199254740991-9007199254740991

我們來測試一下:

let a = 9007199254740986;
for(let i = 0; i < 10; i++) {
	console.log(`${i} : ${a + i}`);
}

複製程式碼

在 chrome 下的輸出結果是這樣的:

0 : 9007199254740986
1 : 9007199254740987
2 : 9007199254740988
3 : 9007199254740989
4 : 9007199254740990
5 : 9007199254740991
6 : 9007199254740992
7 : 9007199254740992
8 : 9007199254740994
9 : 9007199254740996


複製程式碼

看到在 a + i 的值小於等於 9007199254740991 時,輸出正常的每次迴圈加 1 的結果,到了大於 9007199254740991 後,輸出的結果裡出現了兩次 9007199254740992,少了 9007199254740993 和 9007199254740995。這是因為,超過 9007199254740991 之後,JavaScript 的 Number 型別就沒辦法精確地表示整數了。因為丟失了精度,所以 9007199254740993 和 9007199254740995 不見了。

我們可以利用這個知識點構造其他一些滿足需求的值:

let a = Number.MIN_SAFE_INTEGER - 1;
console.log(a === a - 1); // true

複製程式碼

大整數 Big Integer

在最新的 Chrome 瀏覽器下,其實我們可以精確表示大整數,TC39 的 Big Integer 提案目前是 Stage 3 階段,在 Chrome 瀏覽器上已經被支援。

console.log(2 ** 2000); // Infinity
console.log(2n ** 2000n); // 114813069527425452423283320117768198402231770208869520047764273682576626139237031385665948631650626991844596463898746277344711896086305533142593135616665318539129989145312280000688779148240044871428926990063486244781615463646388363947317026040466353970904996558162398808944629605623311649536164221970332681344168908984458505602379484807914058900934776500429002716706625830522008132236281291761267883317206598995396418127021779858404042159853183251540889433902091920554957783589672039160081957216630582755380425583726015528348786419432054508915275783882625175435528800822842770817965453762184851149029376n

複製程式碼

NaN

有同學可能想到 NaN,不過 NaN 與任何值都不相等,包括 NaN 自身,所以,利用 NaN 是不可以的:

let a = NaN;
console.log(a === a - 1); // false

複製程式碼

最後,我們再擴充套件一下,如果面試題要求的不是a === a - 1,而是a == a - 1,那麼有沒有其他答案呢?大家可以思考一下,然後在 github issue 下討論。