前方提醒: 篇幅較長,點個贊或者收藏一下,可以在下一次閱讀時方便查詢
提到js的隱式轉換,很多人第一反應都是:坑。
的確,對於不熟悉的人來說,js隱式轉換
存在著很多的讓人無法預測的地方,相信很多人都深受其害,所以,大家在開發過程中,可能會使用===
來儘量避免隱式轉換。但是,為了更加深入的理解javascript
,本著對知識渴望的精神,我們來通過大量的例子分析分析js隱式轉換
,熟悉js隱式轉換
的規則,讓其在你的眼裡變成“顯式”。
從一道面試題說起
先來看看一個經典的面試題
定義一個變數
a
,使得下面的表示式結果為true
a == 1 && a == 2 && a == 3
複製程式碼
還有這種操作?先試試看吧,定義a = true
?
var a = true
a == 1 && a == 2 && a == 3 // false
複製程式碼
但是並沒有達到預期,好像觸碰到知識盲區了。。。沒關係,先放下吧,來看看幾個更坑的
[] == ![] // true
[] == 0 // true
[2] == 2 // true
['0'] == false // true
'0' == false // true
[] == false // true
[null] == 0 // true
null == 0 // false
[null] == false // true
null == false // false
[undefined] == false // true
undefined == false // false
複製程式碼
一臉懵逼? 不要緊!接下來帶你完完全全的認識javascript的隱式轉換
。
javascript隱式轉換規則
1. ToString,ToNumber,ToBoolean,ToPrimitive
我們需要先了解一下js資料型別之間轉換的基本規則,比如數字、字串、布林型、陣列、物件之間的相互轉換。
1.1 ToString
這裡所說的
ToString
可不是物件的toString方法
,而是指其他型別的值轉換為字串型別的操作。
這裡我們討論null
、undefined
、布林型
、數字
、陣列
、普通物件
轉換為字串的規則。
- null:轉為
"null"
- undefined:轉為
"undefined"
- 布林型別:
true
和false
分別被轉為"true"
和"false"
- 數字型別:轉為數字的字串形式,如
10
轉為"10"
,1e21
轉為"1e+21"
- 陣列:轉為字串是將所有元素按照","連線起來,相當於呼叫陣列的
Array.prototype.join()
方法,如[1, 2, 3]
轉為"1,2,3"
,空陣列[]
轉為空字串,陣列中的null
或undefined
,會被當做空字串處理 - 普通物件:轉為字串相當於直接使用
Object.prototype.toString()
,返回"[object Object]"
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(10) // '10'
String(1e21) // '1e+21'
String([1,2,3]) // '1,2,3'
String([]) // ''
String([null]) // ''
String([1, undefined, 3]) // '1,,3'
String({}) // '[object Objecr]'
複製程式碼
物件的toString
方法,滿足ToString
操作的規則。
注意:上面所說的規則是在預設的情況下,如果修改預設的
toString()
方法,會導致不同的結果
1.2 ToNumber
ToNumber
指其他型別轉換為數字型別的操作。
- null: 轉為
0
- undefined:轉為
NaN
- 字串:如果是純數字形式,則轉為對應的數字,空字元轉為
0
, 否則一律按轉換失敗處理,轉為NaN
- 布林型:
true
和false
被轉為1
和0
- 陣列:陣列首先會被轉為原始型別,也就是
ToPrimitive
,然後在根據轉換後的原始型別按照上面的規則處理,關於ToPrimitive
,會在下文中講到 - 物件:同陣列的處理
Number(null) // 0
Number(undefined) // NaN
Number('10') // 10
Number('10a') // NaN
Number('') // 0
Number(true) // 1
Number(false) // 0
Number([]) // 0
Number(['1']) // 1
Number({}) // NaN
複製程式碼
1.3 ToBoolean
ToBoolean
指其他型別轉換為布林型別的操作。
js中的假值只有false
、null
、undefined
、空字元
、0
和NaN
,其它值轉為布林型都為true
。
Boolean(null) // false
Boolean(undefined) // false
Boolean('') // flase
Boolean(NaN) // flase
Boolean(0) // flase
Boolean([]) // true
Boolean({}) // true
Boolean(Infinity) // true
複製程式碼
1.4 ToPrimitive
ToPrimitive
指物件型別型別(如:物件、陣列)轉換為原始型別的操作。
- 當物件型別需要被轉為原始型別時,它會先查詢物件的
valueOf
方法,如果valueOf
方法返回原始型別的值,則ToPrimitive
的結果就是這個值 - 如果
valueOf
不存在或者valueOf
方法返回的不是原始型別的值,就會嘗試呼叫物件的toString
方法,也就是會遵循物件的ToString
規則,然後使用toString
的返回值作為ToPrimitive
的結果。
注意:對於不同型別的物件來說,
ToPrimitive
的規則有所不同,比如Date物件
會先呼叫toString
,具體可以參考ECMA標準
如果valueOf
和toString
都沒有返回原始型別的值,則會丟擲異常。
Number([]) // 0
Number(['10']) //10
const obj1 = {
valueOf () {
return 100
},
toString () {
return 101
}
}
Number(obj1) // 100
const obj2 = {
toString () {
return 102
}
}
Number(obj2) // 102
const obj3 = {
toString () {
return {}
}
}
Number(obj3) // TypeError
複製程式碼
前面說過,物件型別在ToNumber
時會先ToPrimitive
,再根據轉換後的原始型別ToNumber
Number([])
, 空陣列會先呼叫valueOf
,但返回的是陣列本身,不是原始型別,所以會繼續呼叫toString
,得到空字串
,相當於Number('')
,所以轉換後的結果為"0"
- 同理,
Number(['10'])
相當於Number('10')
,得到結果10
obj1
的valueOf
方法返回原始型別100
,所以ToPrimitive
的結果為100
obj2
沒有valueOf
,但存在toString
,並且返回一個原始型別,所以Number(obj2)
結果為102
obj3
的toString
方法返回的不是一個原始型別,無法ToPrimitive
,所以會丟擲錯誤
看到這裡,以為自己完全掌握了?別忘了,那道面試題和那一堆讓人懵逼的判斷還沒解決呢,本著對知識渴望的精神,繼續往下看吧。
2. 寬鬆相等(==)比較時的隱式轉換規則
寬鬆相等(==)
和嚴格相等(===)
的區別在於寬鬆相等會在比較中進行隱式轉換
。現在我們來看看不同情況下的轉換規則。
2.1 布林型別和其他型別的相等比較
- 只要
布林型別
參與比較,該布林型別
的值首先會被轉換為數字型別
- 根據
布林型別
的ToNumber
規則,true
轉為1
,false
轉為0
false == 0 // true
true == 1 // true
true == 2 // false
複製程式碼
之前有的人可能覺得數字
2
是一個真值,所以true == 2
應該為真,現在明白了,布林型別true
參與相等比較會先轉為數字1
,相當於1 == 2
,結果當然是false
我們平時在使用if
判斷時,一般都是這樣寫
const x = 10
if (x) {
console.log(x)
}
複製程式碼
這裡if(x)
的x
會在這裡被轉換為布林型別,所以程式碼可以正常執行。但是如果寫成這樣:
const x = 10
if (x == true) {
console.log(x)
}
複製程式碼
程式碼不會按照預期執行,因為x == true
相當於10 == 1
2.2 數字型別和字串型別的相等比較
- 當
數字型別
和字串型別
做相等比較時,字串型別
會被轉換為數字型別
- 根據字串的
ToNumber
規則,如果是純數字形式的字串,則轉為對應的數字,空字元轉為0
, 否則一律按轉換失敗處理,轉為NaN
0 == '' // true
1 == '1' // true
1e21 == '1e21' // true
Infinity == 'Infinity' // true
true == '1' // true
false == '0' // true
false == '' // true
複製程式碼
上面比較的結果和你預期的一致嗎? 根據規則,字串轉為數字,布林型也轉為數字,所以結果就顯而易見了。
這裡就不討論
NaN
了,因為NaN
和任何值都不相等,包括它自己。
2.3 物件型別和原始型別的相等比較
- 當
物件型別
和原始型別
做相等比較時,物件型別
會依照ToPrimitive
規則轉換為原始型別
'[object Object]' == {} // true
'1,2,3' == [1, 2, 3] // true
複製程式碼
看一下文章開始時給出的例子
[2] == 2 // true
複製程式碼
陣列[2]
是物件型別,所以會進行ToPrimitive
操作,也就是先呼叫valueOf
再呼叫toString
,根據陣列ToString
操作規則,會得到結果"2"
,
而字串"2"
再和數字2
比較時,會先轉為數字型別,所以最後得到的結果為true
。
[null] == 0 // true
[undefined] == 0 // true
[] == 0 // true
複製程式碼
根據上文中提到的陣列ToString
操作規則,陣列元素為null
或undefined
時,該元素被當做空字串
處理,而空陣列[]
也被轉為空字串
,所以上述程式碼相當於
'' == 0 // true
'' == 0 // true
'' == 0 // true
複製程式碼
空字串
會轉換為數字0
,所以結果為true
。
試試valueOf方法
const a = {
valueOf () {
return 10
}
toString () {
return 20
}
}
a == 10 // true
複製程式碼
物件的
ToPrimitive
操作會先呼叫valueOf
方法,並且a
的valueOf
方法返回一個原始型別的值,所以ToPrimitive
的操作結果就是valueOf
方法的返回值10
。
講到這裡,你是不是想到了最開始的面試題?
物件每次和原始型別做==
比較時,都會進行一次ToPrimitive
操作,那我們是不是可以定義一個包含valueOf
方法的物件,然後通過某個值的累加來實現?
試一試
const a = {
// 定義一個屬性來做累加
i: 1,
valueOf () {
return this.i++
}
}
a == 1 && a == 2 && a == 3 // true
複製程式碼
結果正如你所想的,是正確的。當然,當沒有定義valueOf
方法時,用toString
方法也是可以的。
const a = {
// 定義一個屬性來做累加
i: 1,
toString () {
return this.i++
}
}
a == 1 && a == 2 && a == 3 // true
複製程式碼
2.4 null、undefined和其他型別的比較
null
和undefined
寬鬆相等的結果為true,這一點大家都知道
其次,null
和undefined
都是假值,那麼
null == false // false
undefined == false // false
複製程式碼
居然跟我想的不一樣?為什麼呢? 首先,false
轉為0
,然後呢? 沒有然後了,ECMAScript規範
中規定null
和undefined
之間互相寬鬆相等(==)
,並且也與其自身相等,但和其他所有的值都不寬鬆相等(==)
。
最後
現在再看前面的這一段程式碼就明瞭了許多
[] == ![] // true
[] == 0 // true
[2] == 2 // true
['0'] == false // true
'0' == false // true
[] == false // true
[null] == 0 // true
null == 0 // false
[null] == false // true
null == false // false
[undefined] == false // true
undefined == false // false
複製程式碼
最後想告訴大家,不要一味的排斥javascript的隱式轉換,應該學會如何去利用它,你的程式碼中可能存在著很多的隱式轉換,只是你忽略了它,要做到知其然,並知其所以然,這樣才能有助於我們深入的理解javascript。
(看了這麼久了,辛苦了,不過我也寫了很久啊,點個贊再走吧)