從一道面試題說起—js隱式轉換踩坑合集
前方提醒: 篇幅較長,點個贊或者收藏一下,可以在下一次閱讀時方便查詢
提到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
的結果。
如果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。
(看了這麼久了,辛苦了,不過我也寫了很久啊,點個贊再走吧)