JavaScript 奇怪事件簿

李熠發表於2019-03-01

不好意思做了一回標題黨,JavaScript 中從來就沒有什麼奇怪的事件,我只是想梳理一下 javascript 中讓人疑惑的表示式以及背後的原理。

比如請說出以下這些表示式的結果:

  • 1 + `1`
  • 1 - `1`
  • `2` + `2` - `2`
  • [] + []
  • {} + {}
  • [] + {}
  • {} + []
  • [] + {} === {} + []
  • {} + [] === [] + {}
  • [+false] + [+false] + [+false]
  • [+false] + [+false] + [+false] - [+false]
  • `1` == true
  • parseInt(`infinity`) == 0 / 0
  • 1 < 2 < 3
  • 3 > 2 > 1
  • isNaN(false)
  • isNaN(null)
  • [[][[]]+[]][+[]][++[+[]][+[]]]

如果想知道正確答案的話把表示式貼上到瀏覽器的控制檯執行即可

接下來的內容就是講解這些表示式的結果是在什麼樣的原理下得出的

解決以上的問題的關鍵在於要搞明白三點:

  1. 操作符的使用方法和優先順序
  2. 運算元在操作符的上下文中資料型別轉化規則
  3. 語法中的特例

+ 操作符

+在 JavaScript 中有三個作用:

  1. 連線字串:var result = `Hello` + `World`
  2. 計算數字之和:var result = 1 + 2
  3. 作為一元操作符:+variable

在表示式中+是操作符(operator),操作符操作的物件(上面例子中的HelloWorld12)名為運算元(operand)

一元+操作符的運算規則是:ToNumber(ToPrimitive(operand)),也就是把任意型別都轉化為數字型別。

當運算元的資料型別不一致時,會根據以下規則進行轉化:

  • 如果至少一個運算元是物件資料型別(object),則需要將它轉化為基礎型別(primitive),即字串、數字或者布林
    1. 如果物件是Date型別,那麼呼叫toString()方法
    2. 否則優先呼叫 valueOf() 方法
    3. 如果valueof()方法不存在或者並沒有返回一個基礎型別,那麼呼叫toString()
    4. 當陣列轉化為基礎型別時,JavaScript 會使用join(`,`)方法
    5. 單純的 Javascript 物件 {} 轉化的結果是 [object Object]
  • 轉化之後,如果至少一個運算元是字串型別,那麼另一個運算元也需要轉化為字串型別,然後執行連線操作
  • 在其他的情況下,兩個運算元都轉化為數值型別,並且執行加法操作
  • 如果兩個運算元都是基礎型別,操作符會判斷至少一個是字串型別並且執行連線操作。其他情況都轉化為數字並且求和

所以根據以上規則,我們就能解釋:

  • 1 + `1` 的結果是 `11`,因為其中一個是運算元是字串,所以另一個運算元也被轉化為字串,並且執行字串連線操作
  • [] + [] 的結果是 `` 空字串,因為陣列是物件型別,轉化為基礎型別的結果是空字串,拼接之後仍然是空字串
  • [] + {} 的結果是 [object Object],因為運算元有物件型別的關係,兩個運算元都需要轉化為基礎型別,[]轉化為基礎型別的結果是``{}轉化為基礎型別的結果是[object Object],最後字串拼接的結果仍然是[object Object]

接下來我們說一說值得注意的情況

  • {} + [] 的結果是0。因為在這個表示式中,開頭{}並不是空物件的字面量,而是被當作空的程式碼塊。事實上這個表示式的值就是+[]的結果,即Number([].join(`,`)),即為0

  • 更奇怪的是{} + {}這個表示式,在不同的瀏覽器中執行會得到不同的結果。
    按照上面的例子,我們可以同理推出這個表示式的值實際上是+{}的值,即最後的結果是Number([object Object]),即NaN。在 IE 11 中的執行結果卻是是如此,但是如果在 Chrome 中執行,你得到的結果是 [object Object][object Object]

根據 Stackoverflow上的回答 這是因為 Chrome devtools 在執行程式碼的時候隱式的給表示式新增了括號(),實際上執行的程式碼是({} + {})。如果你在 IE 11 中執行({} + {}),就會得到[object Object][object Object]的結果

  • 雖然上面我們已經明確了 [] + {} 的結果是 [object Object],而 {} + [] 的結果是0,但是如果把他們進行比較的話:[] + {} === {} + []結果會是true。因為右側的{}跟隨在===之後的關係,不再被認為是空的程式碼塊,而是字面量的空物件,所以兩側的結果都是[object Object]

  • {} + [] === [] + {} 同樣是一個有歧義的結果,理論上來說表示式的返回值是false,在 IE 11 中確實如此,但是在 Chrome 的 devtools 中返回 true,原因仍然是表示式被放在()中執行

  • [+false] + [+false] + [+false]的結果也可想而知了,+false的結果是false轉化為數字0,之後[0]又被轉化為基礎型別字串`0`,所以表示式最後的結果是`000`

-操作符

雖然-操作符和+操作符看看上去性質相同,但-操作符只有一個功能,就是數值上的相減。它會嘗試把非數值型別的運算元轉化為數值型別,如果轉化的結果是NaN, 那麼表示式的結果可想而知也就是NaN,如果全部都轉化成功,則執行減法操作,所以

  • 1 - `1` 實際上執行的是 1 - 1,結果為 0
  • `2` + `2` - `2` 表示式首先要遵循從左至右的執行順序,`2` + `2`的執行的是字串拼接,結果是`22`,在接下來的`22` - `2`計算中兩個運算元都成功的轉化為了數字,結果是數字相減的結果20
  • [+false] + [+false] + [+false] - [+false]表示式實際上執行的是`000` - `0`,最後的結果也就是數字0

==操作符

在 JavaScript 中===稱為恆等操作符(The identity operator),==稱為相等操作符(The equality operator)。因為篇幅關係在這裡我們簡單的針對題目聊聊後者

如果==操作符的運算元的資料型別不同:

  1. 如果一個運算元是null,並且另外一個運算元是undefined,他們是相等的
  2. 如果一個運算元是數值型別,並且另一個是字串型別,那麼把字串型別轉化為數值型別再進行比較
  3. 如果一個運算元是布林型別,那麼把true轉化為1,false轉化為0在進行比較
  4. 如果一個運算元是物件,另一個運算元是數字或者字串,那麼把物件轉化為基本型別再進行比較
  • 根據以上規則,在計算表示式`1` == true時,首先將true轉化為數字1,此時表示式中同時存在數值和字串型別,再把字串`1`轉化為數字1,最終1 == 1當然成立
  • 表示式parseInt(`infinity`) == 0 / 0實際上是在判斷NaN == NaN,這樣的比較是一個特例,無論是在==比較還是===比較中,NaN不會與任何東西相等;或者說只要有任意運算元是NaN,那麼表示式就會返回false

更全面=====的比較規則請參考: The legend of JavaScript equality operator

比較運算子><也遵循相似的規則: 1. 優先將字串轉化為數字進行比較;2. 將布林型別轉化為數字再進行比較,

  • 在表示式1 < 2 < 3 中,首先執行1 < 2,結果為true,但是在比較true < 3的過程中,需要把true轉化為數值型別1,最終比較1 < 3,返回值為 true
  • 同理在表示式3 > 2 > 1中,最終比較的其實是true > 1,也即是1 > 1當然返回的是false

isNaN

“NaN”是”Not a Number”的縮寫,我們以為isNaN能夠直接用來判斷值是否是數字型別,但實際上並不可以。因為isNaN首先會強制將引數轉化為數值型別,再進行判斷。
這也就不難解釋為什麼isNaN(false)isNaN(null)返回都是true,因為falsenull都能被成功轉化為數字0, 所以對於isNaN來說,它們是數字

結束

最後我們以表示式[[][[]]+[]][+[]][++[+[]][+[]]]作為文章的結尾

在這個表示式中出現了三種操作符,分別是

  • 成員操作符: []
  • 一元操作符: +
  • 作為求和或者連線字串作用的操作符: +
  • 自增操作符: ++

根據操作符的優先次序表,我們能確定操作符的優先順序依次是: [] > 一元操作符+ > ++ > +

所以根據優先順序我們首先可以計算出表示式的+[]部分,並且將表示式的這一部分用計算結果替換掉: [[][[]]+[]][0][++[0][0]]

接下來我們把表示式拆分為三部分看待: [ [][[]]+[] ] [0] [ ++[0][0] ]。如果還是不清晰的話,三部分從左到右分別是:

  1. [ [][[]]+[] ]
  2. [0]
  3. [ ++[0][0] ]

我們先看第一部分中+前面的 [][[]] 運算元,第一個[]是空陣列,而緊跟著的[[]]是屬性訪問器(成員操作符),屬性訪問器內的[]會被強制轉化為字串型別,最終的結果即是空字串``,所以第一個運算元的最終結果其實是[][``],即是undefined,而又因為+操作符的規則,最終[][[]]+[]表示式的結果是字串`undefined`,那麼現階段表示式的結果是[`undefined`][0][++[0][0]],即`undefined`[++[0][0]]

接下來我們解決第三部分: [++[0][0]],我已經知道成員操作符[]的優先順序要高於自增操作符++, 所以關於表示式++[0][0],我們需要首先計算[0][0],結果是0,之後計算++0的結果即是1

所以最終表示式轉化為了`undefined`[1],最終的結果即是`n`

本文也同時釋出在我的知乎專欄前端技術漫遊指南
上,歡迎大家關注

參考文章

相關文章