瞭解JavaScript中的型別轉換

forceddd發表於2021-10-15

1.什麼是型別轉換?

型別轉換的定義很容易理解,就是值從一個型別轉換為另一個型別,比如說從String型別轉換為Number型別,'42'→42。但是,如果我們對JS中的型別轉換規則瞭解的並不足夠的話,我們就會遇到很多令人迷惑的問題,就好像如果沒有學習物理和化學,生活中處處都是魔法一樣。

JS中的型別轉換大概可以分為顯式型別轉換隱式型別轉換兩種。兩者的區別十分明顯,比如顯式型別轉換'String(42)',我們可以明顯的看出,這行程式碼就是為了將42轉為String型別。然而隱式型別轉換通常是某些操作的副作用,比如''+42,就沒那麼明顯了,它是加法操作的副作用。

如果你沒有理解JS中的型別轉換,下面的幾個例子可能會讓你王德發。

//ex.1
console.log([] == ![]); //true

//ex.2
const a1 = {},
    a2 = {};
console.log(a1 < a2); //false
console.log(a1 == a2); //false
console.log(a1 > a2); //false
console.log(a1 >= a2); //true 
console.log(a1 <= a2); //true

//ex.3
const a3 = {
    i: 1,
    valueOf() {
        return this.i++;
    },
};

console.log(a3 == 1 && a3 == 2); //true

這些奇怪的現象都有背後的道理,當然,前提是你要理解它。

2.型別轉換的抽象操作

在詳細介紹顯式和隱式型別轉換之前,我們需要先知曉StringNumberBoolean之間型別轉換的基本規則。ES5規範中定義了這些型別之間轉換的抽象操作(或者說轉換規則)。下面簡單介紹幾種抽象操作規則。

  1. ToPrimitive(負責物件轉為基本資料型別)

    為了將值轉換為相應的基本型別值,抽象操作 ToPrimitive 會首先檢查該值是否有 valueOf 方法。如果有並且該方法返回基本型別值,就使用該值進行強制型別轉換。

    如果沒有就使用 toString的返回值(如果存在)來進行強制型別轉換。
    如果 valueOftoString返回基本型別值,會產生 TypeError 錯誤。

    例如:

    //ex.4
    const testObj = {};
    console.log(testObj.valueOf()); // {}  testObj本身
    console.log(testObj.toString()); //'[object Object]' string字串
    /*
        首先呼叫testObj的valueOf方法,返回值是testObj本身,不是原始(基本)值型別;所以呼叫testObj的toString方法,返回了字串'[object Object]',是基本型別,所以使用'[object Object]'進行操作。
    
    */
    console.log('' + testObj); // '[object Object]'
    
    testObj.valueOf = () => 'valueOf';
    /*
        此時呼叫testObj的valueOf方法,返回的是字串'valueOf',是基本型別,所以會用該返回值進行操作,而不會再繼續呼叫toString方法了。
    */
    console.log('' + testObj); // 'valueOf'
  1. ToString(負責非字串轉為字串)

    a.當要轉換的值是基本型別時,

    要轉換的值轉成String之後的結果
    null'null'
    undefined'undefined'
    true , false'true' , 'false'
    Symbol('example')僅支援顯式型別轉換得到'Symbol('example')'
    普通數字 0,1,2符合通用規則 '0','1','2'
    極大或極小的數字 1/10000000使用指數形式 '1e-7' ,'1e+21'

    b.當要轉換的值是物件時,如果是顯式型別轉換,則呼叫該值的toString方法,如果是隱式型別轉換,則先通過ToPrimitive操作轉為基本型別,再根據規則a轉為String型別。

    當需要轉換的值的valueOf方法和toString方法沒有被修改時,常用的物件轉換如下:

    要轉換的值轉成String之後的結果
    Object'[object Object]'
    Array [1,2,3]用逗號分隔的每個陣列元素,'1,2,3'
    陣列元素是null或者undefined時, [null],[undefined]空字串 ''
    Date美式英語日期格式的字串,'Fri Oct 15 2021 14:04:28 GMT+0800 (中國標準時間)'
    Function表示函式原始碼的一個字串
  1. ToNumber(負責非數字轉為數字)

    要轉換的值轉成Number之後的結果
    true1
    false0
    undefinedNaN
    null0
    string '42','42px'基本遵循數字常量的相關規則,處理失敗時返回NaN 42,NaN
    symbol不可轉換
    Object等複雜型別先通過ToPrimitive轉為基本型別,再轉成Number型別

    例如:

    console.log(Number([null])); //0
    // [null] 通過ToPrimitive轉成基本型別是空字串 '',空字串轉為Number得到0
  2. ToBoolean

    JavaScript 中的值可以分為以下兩類:
    (1) 可以被型別轉換為 false 的值:undefinednull+0、-0和NaNfalse''
    (2) 其他(被型別轉換為 true 的值),即除了上面的幾個值以外的。

    要注意的是有一個概念稱為假值物件,假值物件和普通物件的區別在於,當他被轉為Boolean時,會得到false。這類情況非常少見,但是確實存在,比如document.all物件,console.log(!!document.all); //false

好了,目前我們已經對這幾種抽象操作有一定了解了,下面就可以稍微深入一下型別轉換了。

3.顯式型別轉換

  1. 字串和數字之間的顯式型別轉換

    ​ 字串和數字之間的型別轉換應該是我們最常見的一種了,它們之間的顯式型別轉換時通過JS中的原生建構函式String和Number實現的,但是通過new機進行建構函式呼叫。這兩個函式在進行型別轉換時,都遵循ToStringToNumber抽象操作。

    例如:

    console.log(Number('3')); // 3
    console.log(String(3)); //'3'
  1. 顯式解析字串

    解析字串中的數字(parseIntparseFloat)和將字串轉換為數字雖然得到的結果都是數字,但是二者的轉換規則是不一樣的。

    ​ a. 相對於Number而言,parseInt允許傳入的字串引數中含有非數字字元,解析按照從左到右的順序,遇到非數字字元就停止解析。例如console.log(parseInt('10px')); //10 ,console.log(Number('10px'));//NaN

    ​ b. parseInt是針對字串的函式,當傳入的第一個引數不是字串時,會先將其轉為字串再進行解析。

    ​ c. parseInt有第二個引數,代表解析時使用的進位制,比如 console.log(parseInt('11', 8)); //9,用八進位制對'11'進行解析。該引數的範圍是2-36,傳入範圍之外的值(如果傳入0,會被設為10進位制),會返回NaN。

    在使用parseInt(str,radix)時,radix並沒有指定預設值(只是大部分情況radix會被預設設為10),也就是說如果你不傳入這個引數的話,得到的結果可能是你意料之外的。比如當str0x或者0X(大寫的X)開頭時,radix會被預設設為16,所以當 console.log(parseInt('0x11')); //17會得到17。

    有個很有趣的例子:console.log([1, 2, 3].map(parseInt)); //[ 1, NaN, NaN ],就是利用了這個進位制引數,結果陣列的三項分別是parseInt(1,0),parseInt(2,1),parseInt(3,2),在第一項中,因為進位制傳入了0,被當為10進位制處理,所以返回了1;第二項中因為進位制傳入了1,不在合法範圍(2-36)內,所以返回了NaN;第三項中,因為進位制傳入2,而要解析的引數是3(二進位制只有0和1的表達才是合法的),所以返回了NaN

  1. 顯示轉換Boolean

    StringNumber一樣, Boolean是顯式的 ToBoolean 型別轉換,遵循ToBoolean操作規則。不過一般!!的方法使用的比較多。例如:console.log(Boolean(1), !!1);

4.隱式型別轉換

隱式型別轉換一般是其他操作的副作用。

  1. 加法操作:

    如果運算元是物件,則先物件通過ToPrimitive操作轉為基本型別,然後再繼續操作。

    如果其中一個運算元是字串(String)型別,則將另一個運算元也轉換為字串(String)型別,進行字串拼接。

    如果運算元中沒有字串(String)型別,則將兩個運算元作為數字(Number)型別,進行加法運算。

    例如:

    // 因為 'hello' 是字串,所以把 1 轉成了 '1' ,然後進行字串拼接
    console.log(1 + 'hello'); // '1hello'
    //因為沒有字串型別的運算元,所以進行加法運算,將 true 轉成數字型別為 1 ,所以結果為2
    console.log(1 + true); // 2
    //因為 [2] 是物件型別,將其通過 ToPrimitive 操作之後,轉成字串 '2' ,進而需要把 1 轉成 '1' ,進行字串拼接
    console.log(1 + [2]); // '12'
  1. 減法、除法、乘法等操作:

    如果運算元是物件,則先物件通過ToPrimitive操作轉為基本型別,然後再繼續操作。

    將兩個運算元作為數字(Number)型別,進行運算。

    例如:

    // [3] - [2]  → '3' - '2' → 3 - 2 =1
    console.log([3] - [2]); // 1
    //3 - 1 =2
    console.log([3] - true); // 2
  1. 將其他型別隱式轉換為Boolean的操作:

    當值作為判斷條件時。如 if語句中的條件判斷表示式; for 迴圈語句中的條件判斷表示式;while 迴圈do..while迴圈中的條件判斷表示式;三目運算子(?:) 中的條件判斷表示式; 邏輯運算子 || 和 && 中的值作為條件判斷表示式。該值會被隱式轉換為Boolean型別進行判斷,遵循ToBoolean操作規則。

  2. 寬鬆相等(==):

    在JS中進行關係比較時,寬鬆相等(==)經常被解釋為:“只比較二者的值是否相等,而不比較型別”,其實這種解釋有些問題,這種解釋中的“值”,應該怎樣理解呢?

    比如我在進行比較console.log(0 == ''); //true,0和空字串'',二者的原始值和型別都不相同,但是卻是寬鬆相等的。

    更為準確地解釋,應該是“當進行寬鬆相等的關係比較時,如果二者型別相同,則僅比較二者的值是否相同即可;如果二者型別不同,則需要先進行型別轉換為同一型別,再比較值是否相同”。

    也就是說,當兩個型別不同的值進行寬鬆相等的比較時,會發生隱式的型別轉換。

    寬鬆相等中,型別不同時的型別轉換規則如下(假設兩個運算元分別為x,y):

    1. 如果x和y的型別分別為字串(String)數字(Number),則將字串型別轉為數字然後比較。

      //先將字串 '1' 轉成了數字 1 ,再進行比較 
      console.log(1 == '1');//true
    2. Boolean型別的運算元,都需要轉成數字(Number)型別。

      //先將 false 轉成了 0,然後再根據規則1,將 '0' 轉成了 0,然後進行比較
      console.log('0' == false);//true
    3. 如果一方是物件型別,則需要通過ToPrimitive抽象操作規則,轉成基本型別以後再繼續比較。

      // 先將物件型別的陣列 [1] 轉成了字串 '1' ,再根據規則1,將字串 '1' 轉成了數字 1,然後進行比較
      console.log([1] == 0);//false
    4. null和undefined是寬鬆相等的,可以說,在寬鬆相等的比較中,null和undefined是一回事。

    另外要了解的是:

    • NaN 不等於 NaN 。
    • +0 等於 -0

  3. 大於(>),小於(<),大於等於(>=),小於等於(<=):

    在進行大於小於這兩種比較時:

    a. 如果有物件型別,首先通過ToPrimitive操作轉成基本型別,在進行比較。

    b. 如果兩者都是字串,則按字元的ASCII碼值比較。

    c. 若果有一方不是字串,則將二者都通過ToNumber抽象操作轉為Number型別之後,再進行比較。

    在進行大於等於小於等於這兩種比較時:

    大於等於代表著不小於,即 a>=b的結果是 !(a<b)。小於等於同理。

5.回顧

瞭解了JS型別轉換規則之後,在回過頭看之前的幾個例子,想來我們都能夠理解這些奇怪的現象了。

ex.1:

//ex.1
console.log([] == ![]); //true
//[] 通過ToPrimitive轉成基本型別,得到 '',  ![]得到false。寬鬆相等比較規則,轉成數字 0
// 即 ''==0 , 空字串''有轉成了數字 0 ,所以最終為true 

ex.2:

//ex.2
const a1 = {},
    a2 = {};
//a1 a2都是物件型別,通過ToPrimitive操作之後,結果都是 '[object Object]'
//顯然 '[object Object]'<'[object Object]' 和 '[object Object]'>'[object Object]'都是false
console.log(a1 < a2); //false
console.log(a1 > a2); //false
//在進行寬鬆相等比較時,二者型別相同,不會進行型別轉換,而是比較二者的值,a1,a2顯然指向的地址不同,所以a1==a2 =也為false
console.log(a1 == a2); //false

//小於等於  大於等於 分別為 大於和小於的結果取反,所以都為true
console.log(a1 >= a2); //true 
console.log(a1 <= a2); //true

ex.3:

//ex.3
const a3 = {
    i: 1,
    valueOf() {
        return this.i++;
    },
};

console.log(a3 == 1 && a3 == 2); //true

//a3是物件型別,通過ToPrimitive操作,轉成基本型別得到 a3.i (即1),所以 a3==1 為true
//但是在上面執行valueOf的時候,i自增了一次,所以在 a3==2的比較時, a3轉為基本型別的值時,得到的就是2了,所以a3==2也為true。

相關文章