理解「Thanks for inventing JavaScript」的每個輸出結果

Eason Lin
9 min readMar 6, 2023

--

原圖

大家好,這陣子我在翻閱《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 為例並做了簡化):

  1. 將傳入的參數轉型為數字
  2. 將最大值設為 -Infinity
  3. 遍歷傳入的參數,如果有 NaN 就直接回傳 NaN,否則與最大值比較,若大於最大值則替換較大者為最大值
  4. 回傳最大值

當我們沒有傳入參數時,步驟 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),它會決定哪個運算被優先進行。在 (!+[]+[]+![]) 中總共出現三種運算子,依照優先序分別為:

  1. 括號運算子
  2. 一元運算子
  3. 二元運算子

() 代表了最高優先的括號運算子,因此裡面的運算會優先進行。接著我們看到 !+[]+[]+![] 這裡可以拆解為:

!+[]  +  []  +  ![]

! 為一元邏輯 NOT 運算子,可以將運算元轉會為布林並求其反值;!+[] 這裡的 + 為一元加法運算子,會將其運算元轉換為數字。[] 轉換為數字後為 00 轉布林並求反值就是 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

在這個例子中,[] 會被轉型為數字,也就是 000 相等所以回傳 true

以上就是關於「Thanks for inventing JavaScript」我個人逐一理解後整理出來的筆記,希望有幫助。如果內文有任何想補充、或你覺得我理解有誤的,也歡迎留言指出。

References:

--

--

Eason Lin
Eason Lin

Written by Eason Lin

Frontend Web Developer | Books

No responses yet