js中為什麼你不敢用 “==”
前言
型別轉換在各個語言中都存在,而在 JavaScript 中由於缺乏對其的瞭解而不慎在使用中經常造成bug被人詬病。為了避免某些場景下的意外,甚至推崇直接使用 Strict Equality( === )來代替 ==。這確實能避免很多bug,但更是一種對語言不理解的逃避(個人觀點)。
引入
先丟擲在 You Don’t Know JavaScript (中) 看到的一個例子
[] == [] // false
[] == ![] // true
{} == !{} // false
{} == {} // false
是不是很奇怪?本文將從書中看到的知識與規範相結合,來詳細說明一下JavaScript在型別轉換時候發生的故事。
型別轉換
很多人喜歡說顯示型別轉換與隱式型別轉換,但個人感覺只是說法上的不同,實質都在發生了型別轉換而已,故不想去區分他們了(感覺一萬個人有一萬種說法)
僅在6大基本型別 null undefined number boolean string object 作討論 symbol未考慮
-
舉個栗子
var a = String(1) var b = Number('1') var c = 1 + '' var d = +'1'
a,b直接呼叫了原生函式,發生了型別轉換。c,d使用了+運算子的一些規則,發生了型別轉換。這些是很簡單的也是我們常用的。
其實真正起作用的,是語言內部對規範中抽象操作的實現,接下來我們所說的
ToString, ToNumber, ToBoolean
等都是抽象操作,而不是JS裡對應的內建函式 -
ToString – 規範9.8
按照以下規則轉化被傳遞的引數
Argument Type Result Undefined “undefined” Null “null” Boolean true -> “true”
false – > “false”Number NaN -> “NaN”
+0 -0 -> “0”
-1 -> “-1”
infinity -> “Infinity”
較大的數科學計數法 (詳見規範9.8.1)String 不轉換 直接返回 Object 1. 呼叫ToPrimitive抽象操作, hint 為 String 將返回值作為 value
2. 返回ToString(value)String(undefined) // "undefined" String(null) // "null" String(true) // "true"
ToPrimitive 抽象操作下面會提及
-
ToNumber – 規範9.3
按照以下規則轉換被傳遞引數
Argument Type Result Undefined NaN Null +0 Boolean true -> 1
false -> +0Number 直接返回 String 如果不是一個字串型數字,則返回NaN(具體規則見規範9.3.1) Object 1. 呼叫ToPrimitive抽象操作, hint 為 Number 將返回值作為 value
2. 返回ToNumber(value) -
ToBoolean – 規範9.2
按照以下規則轉換被傳遞引數
Argument Type Result Undefined false Null false Boolean 直接返回 Number +0 -0 NaN -> false
其他為trueString 空字串(length為0) -> false
其他為trueObject true -
ToPrimitive – 規範9.1
顧名思義,該抽象操作定義了該如何將值轉為基礎型別(非物件),接受2個引數,第一個必填的要轉換的值,第二個為可選的hint,暗示被轉換的型別。
按照以下規則轉換被傳遞引數
Argument Type Result Undefined 直接返回 Null 直接返回 Boolean 直接返回 Number 直接返回 String 直接返回 Object 返回一個物件的預設值。一個物件的預設值是通過呼叫該物件的內部方法[[DefaultValue]]來獲取的,同時傳遞可選引數hint。 -
[[DefaultValue]] (hint) – 規範8.12.8
- 當傳遞的hint為 String 時候,
- 如果該物件的toString方法可用則呼叫toString
- 如果toString返回了一個原始值(除了object的基礎型別)val,則返回val
- 如果該物件的valueOf方法可用則呼叫valueOf方法
- 如果valueOf返回了一個原始值(除了object的基礎型別)val,則返回val
- 丟擲TypeError的異常
- 如果該物件的toString方法可用則呼叫toString
- 當傳遞的hint為 Number 時候,
- 如果該物件的valueOf方法可用則呼叫valueOf方法
- 如果valueOf返回了一個原始值(除了object的基礎型別)val,則返回val
- 如果該物件的toString方法可用則呼叫toString
- 如果toString返回了一個原始值(除了object的基礎型別)val,則返回val
- 丟擲TypeError的異常
- 如果該物件的valueOf方法可用則呼叫valueOf方法
- hint的預設值為Number,除了Date object
- 舉個栗子
var a = {} a.toString = function () {return 1} a.valueOf = function () {return 2} String(a) // "1" Number(a) // 2 a + '' // "2" ??????? +a // 2 a.toString = null String(a) // "2" a.valueOf = null String(a) // Uncaught TypeError: balabala
- 當傳遞的hint為 String 時候,
似乎我們發現了一個很不合規範的返回值,為什麼 a + ''
不應該返回”1″嗎
- 問題的答案其實很簡單 + 操作符會對兩遍的值進行 toPrimitive 操作。由於沒有傳遞 hint 引數,那麼就會先呼叫a.valueOf 得到2後因為+右邊是字串,所以再對2進行ToString抽象操作後與””的字串拼接。
不要畏懼使用 ==
基礎概念已經瞭解了,那麼在 == 中到底發生了什麼樣的型別轉換,而導致了經常產生出乎意料的bug,導致了它臭名昭著。
-
抽象相等 – 規範11.9.3
x == y 判斷規則如下:
- 如果xy型別相同 (與嚴格相等判斷一致,不贅述了,詳見規範)
- 如果 x 為 null y 為 undefined, 返回true
- 如果 x 為 undefined y 為 null, 返回true
- 如果 x 型別為 Number, y 型別為 String, 返回 x == ToNumber(y)
- 如果 x 型別為 String, y 型別為 Number, 返回ToNumber(x) == y
- 如果 x 型別為 Boolean, 返回 ToNumber(x) == y
- 如果 y 型別為 Boolean, 返回 x == ToNumber(y)
- 如果 x 型別為 String 或 Number, y 型別為 Object, 返回 x == ToPrimitive(y)
- 如果 x 型別為 Object, y 型別為 String 或 Number, 返回 ToPrimitive(x) == y
- return false
-
再看引入
[] == [] // false
// 1. 兩遍型別都為 Object,比較引用地址,不同返回false 搞定
[] == ![] // true
// 1. ![]強制型別轉換 變為 [] == false
// 2. 根據規範第7條,返回 [] == ToNumber(false), 即 [] == 0
// 3. 根據規範第9條,返回ToPromitive([]) == 0,陣列的valueOf為本身,不是原始值,則返回toString()即 "" == 0
// 4. 根據規範第5條,返回ToNumber("") == 0, 即 0 == 0
// 5. 根據規範第1條,返回 true
// 下面的不贅述了,分析類似上面
{} == !{} // false
{} == {} // false
我們不難看出以下幾點
- 其實在x y型別相同的時候,== 與 === 沒有任何區別。
- 除了undefined與null, 大多數值都會轉換為相同型別後進行對比,也就是說 === 是 == 某些情況下必經的步驟
引用 << 你不知道的JS(中) >> 中的2句話
- 如果兩遍的值中有 true 或者 false , 千萬不要使用 == (會被轉為數字0,1來進行判斷,會出現一些意外的情況)
- 如果兩遍的值中有[]、””或者0,儘量不要使用 ==
抽象比較
先來看看這個例子
var a = { b: 42 }
var b = { b: 43 }
a < b // false
a == b // false
a > b // false
a <= b // true
a >= b // true
是不是感覺到世界又崩塌了???
讓我們來仔細分析一下
var a = { b: 42 }
var b = { b: 43 }
a < b // false
// 1. 兩遍呼叫ToPrimitive, 返回[object Object] 兩遍一致 返回 false
a == b // false
// 兩遍不同的引用,返回false
a > b // false
// 同 a < b
a <= b // true
// 按規範其實是處理成 !(a > b) 所以為true
a >= b // true
所以在不相等比較的時候,我們最後還是進行手動的型別轉換較為安全
總結
深入瞭解型別轉換的規則,我們就可以很容易取其精華去其糟粕,寫出更安全也更簡