從一道可(變)愛(態)的面試題説起。
上週,我們團隊小仙女同學考了我一道面試題,題目是:在什麼情況下,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_INFINITY
和Number.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 + 1
到2 ** 53 - 1
,即-9007199254740991
到9007199254740991
。超過這個數值的整數,都不能被精確表示。
常量Number.MAX_SAFE_INTEGER
和Number.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 下討論。