隱藏在"=="後面的JavaScript隱式轉換

  逛遍各大程序社區論壇,不少自稱編程“大牛”的人最喜歡調侃的語言就是JavaScript。這門被一週創建出來的動態語言被嘲諷沒有任何編程的嚴謹性,其中最爲被大家們津津樂道的就是JS中的"=="號。實際上你在使用時,經常會發現這樣的情況:

// 一個數字既不是true也不是false
40 == true // false
40 == false // false
// 撲朔迷離的字符0
null == false // false
'0' ==  null // false
'0' ==  false // true

  乍一看非常荒謬且不合邏輯的背後,實際上是有一套嚴謹且規範的隱式轉換規則。今天就來總結整理一下隱藏在"=="後面的JavaScript隱式類型轉換的規律。

隱式與顯式轉換的概念

  所謂類型的隱式轉換的概念,是相對於顯式轉換而產生的一個概念,不明顯的,隱藏的類型轉換,是動態語言獨有的類型轉換的概念。比如我現在要進行一個加法運算,3 + 3。正常情況下,在加號兩邊進行運算的應該是兩個Number類型的值進行計算。而在JS中,加號兩邊實際上是可以允許任何類型的值進行計算的,比如字符串,數組。不是Number類型的值,甚至不是一個類型的值可以進行加法運算,是因爲JS在運算的過程中自動的把加號兩邊的值進行類型轉換,使得兩邊的值可以進行加法的處理。這種自動的類型轉換,就被成爲隱式的類型轉換。而在其他靜態語言當中,實際上需要手動把加好兩邊的值轉換爲Number類型,不然計算就會產生錯誤。這種手動的去操作值進行類型轉換就被成爲顯式的類型轉換。

引用類型到基本型的轉換

  在整理隱式類型轉換的規則前,需要簡單過一下引用類型與基本型之間轉換規則。JS當中所有的引用類型的prototype都是繼承自Object,當引用類型需要轉換爲基本類型的時候,都會調用到Object上面的Symbol.toPrimitive方法進行類型轉換,先把對象轉換爲字符串類型,再進行對應的基本類型轉換。我們來看簡單的例子:

// 聲明數組變量
const demoArray = [1, 2];
// 把對象轉換爲字符串類型
demoArray + ''; // "1,2"

  我們現在來改寫demoArray的Symbol.toPrimitive。

// @param {string} type 將要轉換的數據類型,string類型將會爲default
demoArray[Symbol.toPrimitive] = function(type) {
    console.log(type, 'type');
    return 'changedValue';
};

// 轉換爲字符串
demoArray + '';
// default type
// changedValue

// 轉換爲數字
Number(demoArray);
// number type
// NaN

  我們可以看到,無論引用類型是轉換爲字符串還是數字,都會調用Symbol.toPrimitive方法,該方法會接收到一個參數(將要轉換爲的類型),並且返回轉換的結果。默認的Symbol.toPrimitive方法的邏輯,將會先調用引用類型原型鏈上的的valueOf方法,並返回基本型類的值。如果該方法返回了一個引用類型,將繼續調用toString方法,並返回其值。我們來通過修改demoArray的valueOftoString方法來驗證這一段邏輯。

demoArray.valueOf = function() {
    console.log('valueOf');
    return 'changedValueOf';
}
demoArray.toString = function() {
    console.log('toString');
    return 'changedtoString';
}

// 轉換爲字符串,會發現僅調用了 valueOf 方法
demoArray + '';
// valueOf
// changedValueOf

// 修改valueOf方法返回值爲引用類型
demoArray.valueOf = function() {
    console.log('ObjectValueOf');
    return {};
}

// 繼續轉爲字符串
demoArray + '';
// ObjectValueOf
// 調動到了toString
// toString
// changedtoString

'=='的轉換規律

  '=='的轉換規律,簡單而言可以按照下面順序去記性記憶:

  1. 引用類型與基本類型進行比較時,引用類型永遠先使用Symbol.toPrimitive方法轉換爲基本類型。
  2. 基本類型下,非布爾值與布爾值比較,布爾值自身永遠轉換爲數字類型。
  3. 數字類型和非數字類型比較,非數字類型永遠轉爲數字類型進行比較。

  根據準則一,我們來簡單分析比較一下空數組[]和空對象{}會和哪些值相等。

  首先等號兩端均爲引用類型,需要轉換爲基本類型。我們先來調用數組的Symbol.toPrimitive轉換。數組上面valueOf方法總是返回它自身,toString方法,能夠返回數組的內容項。因此空數組[],在調用Symbol.toPrimitive之後得到的是一個空的字符串’’;而對象在進行Symbol.toPrimitive轉換時,valueOf返回對象自身,toString方法是返回一個'[object type]'的一個字符串。因此兩端在進行基本類型轉換之後,實際上是'''[object Object]'。接下來,根據這個轉換原則,我們可以輕鬆得出[] == '''[object Object]' == {}這兩個式值的答案是true。

  我們再來使用'' == false,來使用一下規則2和規則3。根據第二條原則,false首先需要轉爲數字類型,Number(false)爲0。比較的兩端爲變更爲'' == 0。我們再根據規則3,把數字和其他類型比較時,其他類型需要轉換爲數字類型,來把空字符串變爲數字類型Numer('')0。這樣比較的兩端爲0 == 0,最終結果爲true。

  我們現在來綜合三條規則,來推測一下[] == ![]的結果。這是一個很有趣的等式,根據常理![]應該是[]的相反值,所以按照常理這個等式結果應該是false。但是根據規則,首先應該計算等式右側,也就是![]的值,所有引用類型轉換爲布爾值均爲ture,因此![]的結果爲false。等式兩邊比較變更爲[] == false。根據規則1,我們需要把引用類型變更爲基本類型來和基本類型比較,[]通過Symbol.toPrimitive轉換爲空字符串。等式兩邊變更爲'' == false。根據規則2,布爾值需要轉換爲數字。等式兩邊變更爲'' == 0。根據規則3,我們需要把空字符串變更爲數字0,因此這道結果是0 == 0爲true。

  記得在高中學習的時候,數學老師最常對我們說得話就是讓我們不要想當然,一切題目的答案都需要建立在嚴謹的計算結果和推理上。在不瞭解嚴謹規範和定義的情況下,憑着自己其他語言所謂的經驗進行理所當然的想當然,而肆意在各個公開場所嘲諷詬病JS的“所謂大牛”,不管其技術如何,在做人方面已經已經有所欠缺了。

  JavaScript這門語言動態語言在發展過程中,還需要揹負着沉重的歷史包袱,每次迭代和更新標準,都需要做到向下兼容,從嚴格模式引入開始就不難看出這一點。每天數億計的程序和網頁在使用JS解釋器進行運轉,JS不能像Python一樣,能夠隨隨便便就放棄掉2升級到3。作爲一個前端開發者,要不斷深入到JS的規範和原理當中去,發現JS看似動態不嚴謹的毛病下都是有着嚴謹的規範和成熟的思考的,要爲自己所使用的的語言發聲和感到驕傲。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章