從一道面試題說起—js隱式轉換踩坑合集

麵條__發表於2018-10-16

前方提醒: 篇幅較長,點個贊或者收藏一下,可以在下一次閱讀時方便查詢

提到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方法,而是指其他型別的值轉換為字串型別的操作。

這裡我們討論nullundefined布林型數字陣列普通物件轉換為字串的規則。

  • null:轉為"null"
  • undefined:轉為"undefined"
  • 布林型別:truefalse分別被轉為"true""false"
  • 數字型別:轉為數字的字串形式,如10轉為"10"1e21轉為"1e+21"
  • 陣列:轉為字串是將所有元素按照","連線起來,相當於呼叫陣列的Array.prototype.join()方法,如[1, 2, 3]轉為"1,2,3",空陣列[]轉為空字串,陣列中的nullundefined,會被當做空字串處理
  • 普通物件:轉為字串相當於直接使用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
  • 布林型:truefalse被轉為10
  • 陣列:陣列首先會被轉為原始型別,也就是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中的假值只有falsenullundefined空字元0NaN,其它值轉為布林型都為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標準

如果valueOftoString都沒有返回原始型別的值,則會丟擲異常。

  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
  • obj1valueOf方法返回原始型別100,所以ToPrimitive的結果為100
  • obj2沒有valueOf,但存在toString,並且返回一個原始型別,所以Number(obj2)結果為102
  • obj3toString方法返回的不是一個原始型別,無法ToPrimitive,所以會丟擲錯誤

看到這裡,以為自己完全掌握了?別忘了,那道面試題和那一堆讓人懵逼的判斷還沒解決呢,本著對知識渴望的精神,繼續往下看吧。

2. 寬鬆相等(==)比較時的隱式轉換規則

寬鬆相等(==)嚴格相等(===)的區別在於寬鬆相等會在比較中進行隱式轉換。現在我們來看看不同情況下的轉換規則。

2.1 布林型別和其他型別的相等比較

  • 只要布林型別參與比較,該布林型別的值首先會被轉換為數字型別
  • 根據布林型別ToNumber規則,true轉為1false轉為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操作規則,陣列元素為nullundefined時,該元素被當做空字串處理,而空陣列[]也被轉為空字串,所以上述程式碼相當於

  '' == 0 // true
  '' == 0 // true
  '' == 0 // true
複製程式碼

空字串會轉換為數字0,所以結果為true

試試valueOf方法

  const a = {
    valueOf () {
      return 10
    }
    toString () {
      return 20
    }
  }
  a == 10 // true
複製程式碼

物件的ToPrimitive操作會先呼叫valueOf方法,並且avalueOf方法返回一個原始型別的值,所以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和其他型別的比較

  • nullundefined寬鬆相等的結果為true,這一點大家都知道

其次,nullundefined都是假值,那麼

  null == false // false
  undefined == false // false
複製程式碼

居然跟我想的不一樣?為什麼呢? 首先,false轉為0,然後呢? 沒有然後了,ECMAScript規範中規定nullundefined之間互相寬鬆相等(==),並且也與其自身相等,但和其他所有的值都不寬鬆相等(==)

最後

現在再看前面的這一段程式碼就明瞭了許多

  [] == ![] // 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。

(看了這麼久了,辛苦了,不過我也寫了很久啊,點個贊再走吧)

相關文章