JavaScript 類型轉換

首先從一系列讓JavaScript初學者抓狂的運算提及。javascript

1 + {}
{} + 1
[] + {}
{} + []
[] + []
{} + {}

能所有答對上面的運算結果,沒必要浪費時間繼續閱讀本文了。
若是對某一些的結果還不肯定,請慢慢往下看。java

上面列的全部運算,須要理清如下兩點:express

  • +{}的解析規則;
  • JavaScript是如何進行類型轉換的;

+{}的解析規則

+符號

  • 數字的加法運算,二元運算符
  • 字符串的鏈接運算,二元運算符
  • 正號,一元運算符,強制轉換其餘類型的運算元爲數字類型

{}符號

  • 對象的字面量
  • 區塊語句

加法運算規則

首先,咱們來了解,+符號做爲加號二元運算符的運算規則數組

  1. 使用ToPrimitive轉換左右運算元爲原始數據類型值;
  2. 在第1步轉換後,若是有運算元出現原始數據類型爲「字符串」類型時,則另外一運算元強制轉換爲字符串,而後作字符串鏈接運算;
  3. 其餘狀況時,全部運算元都轉換爲原始數據類型爲「數字」類型,而後作數字相加運算;

ToPrimitive運算

圖片描述

從上圖總結ToPrimitive運算的語法說明:瀏覽器

ToPrimitive(input, PreferredType?)

input表明代入的值,而PreferredType能夠是數字(Number)或字符串(String)其中一種,這會表明「優先的」、「首選的」的要進行轉換到哪種原始類型,轉換的步驟會依這裏的值而有所不一樣。
但若是沒有提供這個值也就是預設狀況,則會設置轉換的hint值爲default。這個首選的轉換原始類型的指示(hint值),是在做內部轉換時由JS視狀況自動加上的,通常狀況就是預設值。ide

PreferredType爲數字(Number)時

轉換步驟爲:spa

  1. 若是input是原始數據類型,則直接返回input
  2. 不然,若是input是個對象時,則調用對象的valueOf()方法,若是能獲得原始數據類型的值,則返回這個值。
  3. 不然,若是input是個對象時,調用對象的toString()方法,若是能獲得原始數據類型的值,則返回這個值。
  4. 不然,拋出TypeError錯誤。
PreferredType爲字符串(String)時

上面的步驟2與3對調,轉換步驟爲:3d

  1. 若是input是原始數據類型,則直接返回input
  2. 不然,若是input是個對象時,則調用對象的toString方法,若是能獲得原始數據類型的值,則返回這個值。
  3. 不然,若是input是個對象時,調用對象的valueOf()方法,若是能獲得原始數據類型的值,則返回這個值。
  4. 不然,拋出TypeError錯誤。

有幾點值得注意:code

  1. 數字是預設的PreferredType
  2. 在通常狀況下,加號運算中的對象要做轉型時,都是先調用valueOf再調用toString;
  3. 例外:Date 對象、Symbol 對象對象

    • Date 對象的預設首選類型是字符串(String);
    • Symbol 能顯示轉爲字符串 String(Symbol) 和布爾值,不能轉爲數字;
模擬代碼
a + b:
    pa = ToPrimitive(a)
    pb = ToPrimitive(b)

    if(pa is string || pb is string)
       return concat(ToString(pa), ToString(pb))
    else
       return add(ToNumber(pa), ToNumber(pb))
V8實現源碼
// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
    if (!IS_SPEC_OBJECT(x)) return x;
    if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
    return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x);
}
  • 能夠看出,Date類型的對象預設值爲字符串(String)。

DefaultNumberDefaultString方法

// ECMA-262, section 8.6.2.6, page 28.
function DefaultString(x) {
   if (!IS_SYMBOL_WRAPPER(x)) {
       if (IS_SYMBOL(x)) throw MakeTypeError(kSymbolToString);
       var toString = x.toString;
       if (IS_SPEC_FUNCTION(toString)) {
           var s = % _CallFunction(x, toString);
           if (IsPrimitive(s)) return s;
       }

       var valueOf = x.valueOf;
       if (IS_SPEC_FUNCTION(valueOf)) {
           var v = % _CallFunction(x, valueOf);
           if (IsPrimitive(v)) return v;
       }
   }
   throw MakeTypeError(kCannotConvertToPrimitive);
}
// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
    var valueOf = x.valueOf;
    if (IS_SPEC_FUNCTION(valueOf)) {
        var v = % _CallFunction(x, valueOf);
        if (IS_SYMBOL(v)) throw MakeTypeError(kSymbolToNumber);
        if (IS_SIMD_VALUE(x)) throw MakeTypeError(kSimdToNumber);
        if (IsPrimitive(v)) return v;
    }
    var toString = x.toString;
    if (IS_SPEC_FUNCTION(toString)) {
        var s = % _CallFunction(x, toString);
        if (IsPrimitive(s)) return s;
    }
    throw MakeTypeError(kCannotConvertToPrimitive);
}

至此,咱們弄清楚了ToPrimitive內部運算規則。

原始數據類型轉換規則表

對於原始數據類型直接的轉換規則就不一一解釋了,能夠參看下表:
圖片描述

運算例子解析

1 + {}

解析爲數字1與空對象{}進行加運算,按照上面的規則,空對象{}按照valueOf -> toString順序,valueOf返回對象自己,因此調用toString返回"[object Object]"爲字符串,根據規則,有一個爲字符串,則進行字符串鏈接運算,數字1強制轉換爲字符串"1",最終結果爲"1[object Object]"

{} + 1

這個例子有坑,由於對於瀏覽器引擎來講,它們會認爲以花括號開頭{的,是一個 區塊語句 的開頭,而不是一個對象字面量的語句,因此會認爲略過第一個{}。因此這個例子至關於+1,加號運算會直接變爲一元正號運算,對數字1強制轉換爲數字類型,結果爲1

[] + {}

[]{}分別調用ToPrimitive方法,返回"""[object Object]",而後作字符串拼接獲得"[object Object]"

{} + []

開始的{}被解析爲區塊語句而略過,至關於+[],而[]轉爲原始數據類型結果爲"",強制轉換""爲數字類型,結果爲0

[] + []

[]{}分別調用ToPrimitive方法,返回"""",而後作字符串拼接獲得""

{} + {}

這個例子有爭議,對於FirefoxEdge瀏覽器來講,他們一以貫之的將第一個{}做爲區塊語句略過,而對於V8引擎系列(ChromeNode.js等)則將第一個{}解析爲對象字面量。
這樣致使結果不一致,Firefox等解析語句爲+{},對空對象{}強制轉爲數字類型,即爲+"[object Object]",將非空字符串轉換爲數字類型,結果爲NaN
Chrome等解析語句爲兩個空對象作加運算,結果也比較好理解:"[object Object][object Object]"

複雜一點的例子

[] == ![]

這是一個不嚴格等於運算,咱們來看轉換過程。

// 第一步,轉換右邊,根據上述原始數據類型轉換規則表,
// 全部對象強制轉 Boolean 類型都是 true,因此 ![] 爲 false
ToPrimitive(![]) >> ToPrimitive(!ToBoolean([])) >>
ToPrimitive(!true) >> ToPrimitive(false) >> 0
// 第二步,轉換左邊
ToPrimitive([]) >> ""
// 第三步,判斷
"" == 0 >> ToNumber("") == 0 >> 0 == 0 >> return true

++[[]][+[]] + [+[]]

進一步,來看這個例子。
很明顯,根據運算符優先級,這個表達式能夠用+分隔爲左右兩個部分++[[]][+[]][+[]]

先看右邊[+[]]

能夠看出這是一個數組裏面有一個元素+[],而+[]即將[]強制轉換爲數字類型,因此等於+"",結果爲0
綜上,右邊表達式轉換爲[0]

再看左邊++[[]][+[]]

咱們來一步步拆解, 根據對右邊表達式的轉換,這個表達式能夠等同看作++([[]][0])++後面又能夠看作數組去第1個元素,表達式轉換爲++[]
可是當咱們去瀏覽器執行++[]時,報錯了:<font color=red>Uncaught ReferenceError: Invalid left-hand side expression in prefix operation</font>
嚇得我趕忙去看++的語法,原來++的運算是一種引用運算,即++[]應該轉換爲:

var ref = []
ref = ref + 1

因此++[]轉換的正確姿式爲[] + 1

加法運算

左右進行相加獲得:[] + 1 + [0]
根據ToPrimitive運算規則,[] + 1 + [0] === "" + 1 + [0] === "1" + [0] === "10"

相關文章
相關標籤/搜索