更新時(shí)間:2018年08月06日15時(shí)42分 來源:傳智播客 瀏覽次數(shù):
文章引用:http://0313.name/archives/480
前言
類型轉(zhuǎn)換在各個(gè)語言中都存在,而在 JavaScript 中由于缺乏對(duì)其的了解而不慎在使用中經(jīng)常造成bug被人詬病。為了避免某些場(chǎng)景下的意外,甚至推崇直接使用 Strict Equality( === )來代替 ==。這確實(shí)能避免很多bug,但更是一種對(duì)語言不理解的逃避(個(gè)人觀點(diǎn))。
引入
先拋出在 You Don’t Know JavaScript (中) 看到的一個(gè)例子
[] == [] // false
[] == ![] // true
{} == !{} // false
{} == {} // false
是不是很奇怪?本文將從書中看到的知識(shí)與規(guī)范相結(jié)合,來詳細(xì)說明一下JavaScript在類型轉(zhuǎn)換時(shí)候發(fā)生的故事。
類型轉(zhuǎn)換
很多人喜歡說顯示類型轉(zhuǎn)換與隱式類型轉(zhuǎn)換,但個(gè)人感覺只是說法上的不同,實(shí)質(zhì)都在發(fā)生了類型轉(zhuǎn)換而已,故不想去區(qū)分他們了(感覺一萬個(gè)人有一萬種說法)
僅在6大基本類型 null undefined number boolean string object 作討論 symbol未考慮
舉個(gè)栗子
var a = String(1)
var b = Number('1')
var c = 1 + ''
var d = +'1'
a,b直接調(diào)用了原生函數(shù),發(fā)生了類型轉(zhuǎn)換。c,d使用了+運(yùn)算符的一些規(guī)則,發(fā)生了類型轉(zhuǎn)換。這些是很簡(jiǎn)單的也是我們常用的。
其實(shí)真正起作用的,是語言內(nèi)部對(duì)規(guī)范中抽象操作的實(shí)現(xiàn),接下來我們所說的 ToString, ToNumber, ToBoolean等都是抽象操作,而不是JS里對(duì)應(yīng)的內(nèi)置函數(shù)
ToString – 規(guī)范9.8
按照以下規(guī)則轉(zhuǎn)化被傳遞的參數(shù)
Argument Type Result
Undefined “undefined”
Null “null”
Boolean true -> “true”
false – > “false”
Number NaN -> “NaN”
+0 -0 -> “0”
-1 -> “-1”
infinity -> “Infinity”
較大的數(shù)科學(xué)計(jì)數(shù)法 (詳見規(guī)范9.8.1)
String 不轉(zhuǎn)換 直接返回
Object 1. 調(diào)用ToPrimitive抽象操作, hint 為 String 將返回值作為 value
2. 返回ToString(value)
String(undefined) // "undefined"
String(null) // "null"
String(true) // "true"
ToPrimitive 抽象操作下面會(huì)提及
ToNumber – 規(guī)范9.3
按照以下規(guī)則轉(zhuǎn)換被傳遞參數(shù)
Argument Type Result
Undefined NaN
Null +0
Boolean true -> 1
false -> +0
Number 直接返回
String 如果不是一個(gè)字符串型數(shù)字,則返回NaN(具體規(guī)則見規(guī)范9.3.1)
Object 1. 調(diào)用ToPrimitive抽象操作, hint 為 Number 將返回值作為 value
2. 返回ToNumber(value)
ToBoolean – 規(guī)范9.2
按照以下規(guī)則轉(zhuǎn)換被傳遞參數(shù)
Argument Type Result
Undefined false
Null false
Boolean 直接返回
Number +0 -0 NaN -> false
其他為true
String 空字符串(length為0) -> false
其他為true
Object true
ToPrimitive – 規(guī)范9.1
顧名思義,該抽象操作定義了該如何將值轉(zhuǎn)為基礎(chǔ)類型(非對(duì)象),接受2個(gè)參數(shù),第一個(gè)必填的要轉(zhuǎn)換的值,第二個(gè)為可選的hint,暗示被轉(zhuǎn)換的類型。
按照以下規(guī)則轉(zhuǎn)換被傳遞參數(shù)
Argument Type Result
Undefined 直接返回
Null 直接返回
Boolean 直接返回
Number 直接返回
String 直接返回
Object 返回一個(gè)對(duì)象的默認(rèn)值。一個(gè)對(duì)象的默認(rèn)值是通過調(diào)用該對(duì)象的內(nèi)部方法[[DefaultValue]]來獲取的,同時(shí)傳遞可選參數(shù)hint。
[[DefaultValue]] (hint) – 規(guī)范8.12.8
當(dāng)傳遞的hint為 String 時(shí)候,
如果該對(duì)象的toString方法可用則調(diào)用toString
如果toString返回了一個(gè)原始值(除了object的基礎(chǔ)類型)val,則返回val
如果該對(duì)象的valueOf方法可用則調(diào)用valueOf方法
如果valueOf返回了一個(gè)原始值(除了object的基礎(chǔ)類型)val,則返回val
拋出TypeError的異常
當(dāng)傳遞的hint為 Number 時(shí)候,
如果該對(duì)象的valueOf方法可用則調(diào)用valueOf方法
如果valueOf返回了一個(gè)原始值(除了object的基礎(chǔ)類型)val,則返回val
如果該對(duì)象的toString方法可用則調(diào)用toString
如果toString返回了一個(gè)原始值(除了object的基礎(chǔ)類型)val,則返回val
拋出TypeError的異常
hint的默認(rèn)值為Number,除了Date object
舉個(gè)栗子
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
似乎我們發(fā)現(xiàn)了一個(gè)很不合規(guī)范的返回值,為什么 a + ''不應(yīng)該返回”1″嗎
問題的答案其實(shí)很簡(jiǎn)單 + 操作符會(huì)對(duì)兩遍的值進(jìn)行 toPrimitive 操作。由于沒有傳遞 hint 參數(shù),那么就會(huì)先調(diào)用a.valueOf 得到2后因?yàn)?右邊是字符串,所以再對(duì)2進(jìn)行ToString抽象操作后與””的字符串拼接。
不要畏懼使用 ==
基礎(chǔ)概念已經(jīng)了解了,那么在 == 中到底發(fā)生了什么樣的類型轉(zhuǎn)換,而導(dǎo)致了經(jīng)常產(chǎn)生出乎意料的bug,導(dǎo)致了它臭名昭著。
抽象相等 – 規(guī)范11.9.3
x == y 判斷規(guī)則如下:
如果xy類型相同 (與嚴(yán)格相等判斷一致,不贅述了,詳見規(guī)范)
如果 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. ![]強(qiáng)制類型轉(zhuǎn)換 變?yōu)?[] == false
// 2. 根據(jù)規(guī)范第7條,返回 [] == ToNumber(false), 即 [] == 0
// 3. 根據(jù)規(guī)范第9條,返回ToPromitive([]) == 0,數(shù)組的valueOf為本身,不是原始值,則返回toString()即 "" == 0
// 4. 根據(jù)規(guī)范第5條,返回ToNumber("") == 0, 即 0 == 0
// 5. 根據(jù)規(guī)范第1條,返回 true
// 下面的不贅述了,分析類似上面
{} == !{} // false
{} == {} // false
我們不難看出以下幾點(diǎn)
其實(shí)在x y類型相同的時(shí)候,== 與 === 沒有任何區(qū)別。
除了undefined與null, 大多數(shù)值都會(huì)轉(zhuǎn)換為相同類型后進(jìn)行對(duì)比,也就是說 === 是 == 某些情況下必經(jīng)的步驟
引用 << 你不知道的JS(中) >> 中的2句話
如果兩遍的值中有 true 或者 false , 千萬不要使用 == (會(huì)被轉(zhuǎn)為數(shù)字0,1來進(jìn)行判斷,會(huì)出現(xiàn)一些意外的情況)
如果兩遍的值中有[]、””或者0,盡量不要使用 ==
抽象比較
先來看看這個(gè)例子
var a = { b: 42 }
var b = { b: 43 }
a < b // false
a == b // false
a > b // false
a <= b // true
a >= b // true
是不是感覺到世界又崩塌了???
讓我們來仔細(xì)分析一下
var a = { b: 42 }
var b = { b: 43 }
a < b // false
// 1. 兩遍調(diào)用ToPrimitive, 返回[object Object] 兩遍一致 返回 false
a == b // false
// 兩遍不同的引用,返回false
a > b // false
// 同 a < b
a <= b // true
// 按規(guī)范其實(shí)是處理成 !(a > b) 所以為true
a >= b // true
所以在不相等比較的時(shí)候,我們最后還是進(jìn)行手動(dòng)的類型轉(zhuǎn)換較為安全
總結(jié)
深入了解類型轉(zhuǎn)換的規(guī)則,我們就可以很容易取其精華去其糟粕,寫出更安全也更簡(jiǎn)潔可讀的代碼。
作者:傳智播客前端與移動(dòng)開發(fā)培訓(xùn)學(xué)院
首發(fā):http://web.itcast.cn
北京校區(qū)