大家好,這陣子我在翻閱《Clean Code 學派的風格實踐》時發現它記錄了一些 JS 的特性,翻著翻著就想到了上面這張圖。這篇文我們試著來理解並拆解一下「Thanks for inventing JavaScript」這張有名的迷因圖。
typeof NaN // “number”
在 JavaScript 中,數字型別(number type)總共有 18,437,736,874,454,810,627 種值,而 NaN 僅代表它「不存在於這個值的範圍」,其依然被歸類在 number,儘管它代表 “Not-a-Number”。
此外,根據 EMCAScript 的敘述:
to ECMAScript code, all NaN values are indistinguishable from each other.
每個 NaN 都無法和另一個 NaN 進行比較,這也是為什麼
NaN === NaN
會回傳 false
。
9999999999999999 // 10000000000000000
在 JavaScript 中,數字以雙精度浮點數被儲存,它能儲存的數字有值的上限。以正整數來說,它能精準表示的最大整數為:
Number.MAX_SAFE_INTEGER // 9007199254740991
9999999999999999
因為大於 Number.MAX_SAFE_INTEGER
,超出了其精度限制,因此被儲存為它最接近的可表示數字。
0.5 + 0.1 == 0.6 // true; 0.1 + 0.2 == 0.3 // false
如果我們打開 Console 並輸入
0.1 + 0.2
得到的結果會是 0.30000000000000004
。這是因為 JavaScript 在浮點數(帶有小數的數值)的運算上遵從了IEEE二進位浮點數算術標準(IEEE 754)。實際上在轉二進制後,只要不是 1/2, 1/4, 1/8… 等小數運算上都會有誤差,但因為 IEEE 754 在運算後只取小數點後 52 位,因此才會有「部份正確、部份錯誤」的狀況。以下的情況也會回傳 true:
0.5 + 0.1 == 0.6 // true
0.2 + 0.3 == 0.5 // true
Math.max() // -Infinity; Math.min() // Infinity
我看到這段時也覺得很詭異,後來去看了 EMCAScript Spec 的說明才大概有個底。我們先看一下EMCAScript Spec 所述的實作流程(以 Math.max 為例並做了簡化):
- 將傳入的參數轉型為數字
- 將最大值設為 -Infinity
- 遍歷傳入的參數,如果有 NaN 就直接回傳 NaN,否則與最大值比較,若大於最大值則替換較大者為最大值
- 回傳最大值
當我們沒有傳入參數時,步驟 3 就被略過,所以最終回傳了 -Infinity。對於為什麼需要第二個步驟,我在網路上找到一篇文,大致的意思我理解為:「因為它底層的實作在有傳入參數時就必須進行比較的動作」,不過這篇文的敘述看下來是作者自己思考後得到的結論,如果閱讀這篇文的你知道答案的話也歡迎留言告訴我!
[] + [] // “”
當我們對物件(這裡的物件是廣義上的物件而非鍵值對)使用加法運算子時,JavaScript 會將兩個運算元都轉換為純值(Primitive)後才進行運算。也就是說,一旦使用了加法運算子,不論運算元為物件或是純值,結果必定為純值。
在加法運算子中,若傳入的運算元為陣列,JavaScript 會在一般情況會透過使用 toString()
將陣列轉換為字串,[]
的轉換結果為 ""
,因此 [] + []
會等同 "" + ""
:
[] + [] === '' + '' // true
註記:事實上 JavaScript 在物件的轉型上比較複雜,這篇文基於篇幅僅會概括到迷因圖中的狀況。例如可以試試看:
var a = []
a.valueOf = () => 'hey!'
a + [] // ?
// 或者:
var b = []
b.toString = () => ({})
b + [] // ?
關於轉型更詳細的運作也可以參考這篇文。
[] + {} // “[object Object]”
如同上述,在一般情況 JavaScript 也會透過使用 toString()
將鍵值對轉為字串,鍵值對轉為字串時會變成 "[object Object]"
因此會等於:
"" + "[object Object]"
小小題外話:alert 也會對傳入的參數進行類似的行為,所以當網頁中出現了 “[object Object]” 的瀏覽器彈窗,就有可能是前端或後端把 Response 中某一個鍵的的型別接錯或傳錯了。
{} + [] // 0
這項我覺得非常有趣,原本以為加法運算子的運作已經夠複雜了,竟然還有例外?不過去查了一下才發現這是一個小小的障眼法。在 JavaScript 中,若以 {
在該行作為開頭,它會將其理解為區塊(block)。這個狀況下,其實 {}
代表的並不是一個空的鍵值對,而是一個空的程式碼區塊(empty code block)這裡其實等同:
+[]
也就是將空陣列轉為數字,而空陣列轉為數字的結果會是 0
。
true + true + true === 3 // true
若傳入的運算元均為布林值,JavaScript 會將其轉為數字,true
轉為數字為 1
。因此上述的結果等同:
1 + 1 + 1 // 3
true - true // 0
減法運算子會將運算元轉換為數字,因此 true
相減等同:
1 - 1 // 0
true == 1 // true
在使用抽象相等運算子時,若運算元屬於同一種類型,則直接比較兩邊的值是否相同;當類型不同時則會依照運算元的類型進行轉型後比較。以上面的例子來說,布林值會被轉換為數字,因此上面的例子就會像:
1 == 1 // true
抽象相等運算子實際的運作相對複雜,例如這樣的情況:
const o = {
val: 1,
toString() {
return this.val
}
}
o == 1 // ?
const p = {
val: 1
}
p == 1 // ?
對於抽象相等運算子,我個人的想法是:除非每個會經手這段程式碼的人都知道為何這麼做,否則還是使用嚴格相等吧!
true === 1 // false
嚴格相等運算子只有在運算元「完全相同」時才會回傳 true
。(個人覺得這應該是這張迷因圖中大家最熟悉原因的一條了)
(!+[]+[]+![]).length // 9
JavaScript 中有一個運算子優先序(Operator precedence),它會決定哪個運算被優先進行。在 (!+[]+[]+![])
中總共出現三種運算子,依照優先序分別為:
- 括號運算子
- 一元運算子
- 二元運算子
()
代表了最高優先的括號運算子,因此裡面的運算會優先進行。接著我們看到 !+[]+[]+![]
這裡可以拆解為:
!+[] + [] + ![]
!
為一元邏輯 NOT 運算子,可以將運算元轉會為布林並求其反值;!+[]
這裡的 +
為一元加法運算子,會將其運算元轉換為數字。[]
轉換為數字後為 0
,0
轉布林並求反值就是 true
。
接著我們看最後面的 ![]
,陣列轉為布林一律為 true
,求反值則為 false
。我們的運算等同:
true + [] + false
如同上述,加法運算子在碰到陣列時,一般狀況下會透過 toString()
將其轉為字串,因此 true + []
就等同 true + ""
,當一個運算元是字串時,JavaScript 會將另一個運算元轉換為字串,變成 "true"
。
而 "true" + false
就會變成 "truefalse"
,最後對字串求長度回傳 9
。
9 + “1” // “91”
如上述,在加法運算子中,當一個運算元是字串時,JavaScript 會將另一個運算元轉換為字串。
91 - “1” // 90
如上述,減法運算子會將運算元轉換為數字。字串 "1"
轉回數字後相減得到 90
。
[] == 0 // true
在這個例子中,[]
會被轉型為數字,也就是 0
,0
與 0
相等所以回傳 true
。
以上就是關於「Thanks for inventing JavaScript」我個人逐一理解後整理出來的筆記,希望有幫助。如果內文有任何想補充、或你覺得我理解有誤的,也歡迎留言指出。
References: