淺析js中的類型轉換

前言

衆所周知, js 是一門弱類型或者說是動態語言。變量沒有類型限制,能夠隨時賦予任意值。javascript

雖然變量的數據類型是不肯定的,可是各類運算符對數據類型是有要求的。若是運算符發現,運算值的類型與預期不符,就會自動轉換類型。java

4 + '1' // '41'

上述代碼中,數值 4和字符串'1'相加,結果爲'41',在運算過程當中,JavaScript 將數值4自動轉換爲了字符串'4',相似的轉換在 JavaScript 中很常見,咱們有必要了解下背後的轉換原理。express

js數據類型

js 中的數據類型分兩大類:數組

  • 簡單數據類型(也稱爲基本數據類型):Undefined、Null 、Boolean、Number 和 String
  • 複雜數據類型:Object

總共 6 種數據類型(ES6 新增了 Symobal 類型,本文不討論),相較於其餘的語言,js 中的數據類型好像不足以表示全部數據類型。可是,因爲 js 的數據類型具備動態性,所以沒有再定義其餘數據類型的必要了。瀏覽器

在類型轉換過程當中簡單數據類型又稱爲原始值(原始類型)函數

強制轉換

JavaScript 內置三個轉型函數,可使用Number()String()Boolean(),手動將各類類型的值,分別轉換成數字、字符串或者布爾值。工具

Number()

其實把非數值轉換爲數值的函數除了Number()parseInt()parseFloat()也常常用到。轉型函數Number()能夠用於任何數據類型,然後面兩個專門用於把字符串轉換爲數值。測試

原始類型值
  • 布爾值,true false 分別被轉換爲10
  • 數字值,原樣返回
  • null 值,返回0
  • undefined,返回NaN
  • 字符串eslint

    • 若是是空字符串,返回0
    • 若是是字符串中只包含數字(包括前面帶正負號的狀況),則將其轉換爲十進制數值(例如"123"轉換爲123),前面的零會被忽略(例如"011"轉換爲11
    • 若是字符串中是有效的浮點數,如"1.2",返回1.2(前面的零會被忽略,例如"01.2"轉換爲1.2
    • 若是是有效的十六進制格式,如"0xf",則將其轉換爲相同大小的十進制整數值
    • 其餘格式的字符串,則返回 NaN
Number(true) // 1
Number(false) // 0
Number(10) // 10
Number(null) // 0
Number(undefined) // NaN
Number('') // 0
Number('10') // 10
Number('010') // 10
Number('1.2') // 1.2
Number('01.2') // 1.2
Number('0xf') // 15
Number('hello') // NaN
對象

對象類型的轉換稍微有點複雜:code

  1. 調用對象自身的valueOf()方法,若是返回原始類型的值,則直接對該值使用Number函數,再也不進行後續步驟。
  2. 若是valueOf()方法返回對象,則調用對象自身的toString()方法。若是toString()方法返回原始類型的值,則直接對該值使用Number()函數,再也不進行後續步驟。
  3. 若是toString()方法返回的是對象,則報錯
var obj = {a: 1}
Number(obj) // NaN

// 等同於下面步驟
if (typeof obj.valueOf() === 'object') {
    Number(obj.toString())
} else {
    Number(obj.valueOf())
}

上面代碼中,首先調用valueOf(),結果返回對象自己;繼續調用toString(),返回字符串[object Object],對此字符串調用Number(),返回NaN

大多數狀況下,對象的valueOf()方法返回對象自身,因此通常會調用toString()方法,而toString()方法通常返回對象的類型字符串(例如[object Object]),因此Number()的參數是對象時,通常返回NaN

若是toString()方法返回的不是原始類型的值,就會報錯,咱們能夠重寫對象的toString()方法:

var obj = {
    valueOf: function() {
        return {}
    },
    toString: function() {
        return {}
    }
}
Number(obj)
// TypeError: Cannot convert object to primitive value

數組類型的狀況有點不同,對於空數組,轉換爲0;對於只有一個成員(且該成員爲可以被Number()轉換的值即轉換結果不爲NaN)的數組,轉換爲該成員的值;其餘狀況下的數組,轉換爲NaN

Number([]) // 0
Number([10]) // 10
Number([null]) // 0
Number([[10]]) // 10

數組類型和通常對象的轉換結果不同,主要是由於數組的toString()方法內部重寫了,直接調用不會返回[object Array],而是返回每一個成員(會先轉換成字符串)拼接成的字符串,中間以逗號分隔。

[10].toString() // '10'
[null].toString() // ''
[1, 10].toString() // '1,10'
parseInt()

由於Number()函數在轉換字符串時比較複雜並且不太適用,所以在處理字符串轉換爲整數的時候更經常使用的是parseInt()函數。parseInt()函數在轉換字符串時,更多的是看起其是否符合數值模式。它會忽略字符串前面的空格,直至找到第一個非空格字符。

  • 若是第一個字符不是數字字符或者負號,parseInt()就會返回NaN;所以,用parseInt()轉換空字符串會返回NaNNumber()對空字符串返回0)。
  • 若是第一個字符是數字字符,parseInt()會繼續解析第二個字符,直到解析完全部後續字符或者遇到了一個非數字字符。例如,'123abc'會被轉換爲1234,由於'abc'會被徹底忽略;'22.5'會被轉換爲22,由於小數點不是有效的數字字符。
  • 若是字符串中的第一個字符是數字字符,parseInt()也可以識別出各類整數格式(十進制、八進制、十六進制)。若是字符串以'0x'開頭且後面是數字字符,就會當成一個十六進制整數;若是字符串以0開頭且後面是數字字符,則會將其當作八進制整數。
parseInt('123abc') // 123
parseInt('') // NaN
parseInt('a12') // NaN
parseInt('0xa') // 10(十六進制數)
parseInt('12.5') // 12
parseInt('070') // 56(八進制數)

上述代碼中parseInt('070')在瀏覽器中測試會返回10,前面的零會被忽略掉。其實parseInt()能夠傳入第二個參數,轉換的基數(想要以什麼進制來轉換)

parseInt('0xaf') // 175
parseInt('af') // NaN
// af 原本是有效的十六進制數值,若是指定第二個參數爲16,使用十六進制來解析,那麼能夠省略前面的 0x
parseInt('af', 16) // 175

若是不指定基數,parseInt()會自行肯定如何解析輸入的字符串,爲了不沒必要要的錯誤,應該老是傳入第二個參數,明確指定基數。

通常狀況下,解析的都是十進制數值,應該始終將 10 做爲第二個參數(在語法檢查工具 ESLint中,也會提示指定第二個參數)
parseFloat()

parseInt()函數相似,parseFloat()也是從第一個字符開始解析每一個字符,一直解析到最後一個字符。若是碰見一個無效的浮點數字字符,就會中止解析,輸出結果。字符串中的第一個小數點是有效的,而第二個小數點就是無效的了,所以它後面的字符都會被忽略。例如,'12.34.5'會被轉換爲12.34

parseFloat()會忽略最前面的零,且它只能解析十進制值(沒有第二個參數)。

parseFloat('123abc') // 123
parseFloat('0xa') // 0
parseFloat('12.3') // 12.3
parseFloat('12.3.4') // 12.3
parseFloat('012.3') // 12.3

Boolean()

Boolean()函數能夠將任意類型的值轉爲布爾值。至於返回的這個值是true仍是false,取決於要轉換值的數據類型及其實際值。下表給出了各類數據類型及其對應的轉換規則。

數據類型 轉換爲true的值 轉換爲false的值
Boolean true false
String 任何非空字符串 ""(空字符串)
Number 任何非零數字值(包括無窮大) 0(包括-0和+0)和NaN
Object 任何對象 null
Undefined - undefined

須要注意的是,全部空對象的轉換結果都是true,並且布爾對象表示的false值(new Boolean(false))的轉換結果也是true

Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

String()

String()函數能夠將任意類型的值轉化成字符串,轉換規則以下。

原始類型值
  • 數值,轉爲相應的字符串
  • 字符串,原樣輸出
  • 布爾值,true轉換爲字符串'true'false轉換爲字符串'false'
  • undefined,轉爲字符串'undefined'
  • null,轉爲字符串'null'
String(123) // '123'
String('abc') // 'abc'
String(true) // 'true'
String(undefined) // 'undefined'
String(null) // 'null'
對象

String()函數的參數若是是對象,返回一個類型字符串;若是是數組,返回該數組的字符串形式。

String()函數背後的轉換規則,與Number()函數基本相同,區別是互換了valueOf()方法和toString()方法的執行順序。

  1. 先調用對象自身的toString()方法。若是返回原始類型的值,則對該值使用String()函數,再也不進行如下步驟。
  2. 若是toString()方法返回的是對象,再調用對象的valueOf()方法。若是valueOf()方法返回原始類型的值,則對該值使用String()函數,再也不進行如下步驟。
  3. 若是valueOf()方法返回的是對象,就報錯。
String({name: 1}) // '[object Object]'
// 等同於
String({name: 1}.toString())
String('[object Object]')  // '[object Object]'

上面代碼先調用對象的toString()方法,會返回字符串'[object Object]',而後對該值使用String()函數,不會再調用valueOf()方法,輸出'[object Object]'

若是toString()方法和valueOf()方法,返回的都是對象,就會報錯,能夠重寫對象的這兩個方法來驗證:

var obj = {
  valueOf: function() {
    return {}
  },
  toString: function() {
    return {}
  }
}
String(obj)
// TypeError: Cannot convert object to primitive value

自動轉換

類型轉換中一個難點就是自動轉換(也稱隱式轉換),因爲在一些操做符下其類型會自動轉換,這使得 js 尤爲靈活,但同時又難以理解。

JavaScript 會自動轉換數據類型的狀況主要有三種:

  • 不一樣類型的數據互相運算

    1 == '1' // true
    1 + 'a' // '1a'
  • 條件控制語句的條件表達式爲非布爾值類型

    if ('a') {
      console.log('success')
    }
    // 'success'
  • 對非數值類型的值使用一元運算符(+-

    +{a: 1} // NaN
    -[1] // -1

自動轉換的規則是這樣的:預期什麼類型的值,就調用該類型的轉換函數。好比,某個位置預期爲字符串,就調用String()函數進行轉換。若是該位置便可以是字符串,也多是數值,那麼默認轉爲數值。

因爲自動轉換具備不肯定性,並且不易排錯,建議在預期爲布爾值、數值、字符串的地方,所有使用Boolean()Number()String()函數進行顯式轉換。

ToBoolean

JavaScript 遇到預期爲布爾值的地方(好比if語句的條件部分),就會將非布爾值自動轉換爲布爾值,自動調用Boolean()函數,其轉換規則在上面已經說過。

這些轉換規則對於條件類語句很是重要,在運行過程當中,會自動執行相應的Boolean轉換。

var str = 'abc'
if (str) {
    console.log('success')
} // 'success'
str && 'def' // 'def'

運行上面代碼,就會輸出字符串success,由於字符串abc被自動轉換成了對應的Boolean值(true)。因爲存在這種自動執行的Boolean轉換,所以在條件類語句中知道是什麼類型的變量很是重要。

常見的自動轉換布爾值還有下面兩種:

// 三目運算符
expression ? true : false

// 雙重取反
!!expression

上面兩種寫法,內部其實仍是調用的Boolean()函數。

ToString

JavaScript 遇到預期爲字符串的地方,就會將非字符串值自動轉爲字符串。字符串的自動轉換,主要在字符串的加法運算,當一個值爲字符串,另外一個值爲非字符串時,非字符串會自動轉換。

自動轉換通常會調用非字符串的toString()方法轉換爲字符串(nullundefined沒有toString()方法,調用String()方法轉換爲'null''undefined'),轉換完成獲得相應的字符串值,而後就是執行加法運算,即字符串的拼接操做了。

'1' + 2 // '12'
'1' + true // '1true'
'1' + false // '1false'
'1' + {} // '1[object Object]'
// 數組的 toString 方法通過重寫,返回每一個成員以逗號分隔拼接成的字符串
// 下面一行等同於, '1' + '', 結果爲 '1'
'1' + [] // '1'
'1' + function (){} // '1function (){}'
'1' + undefined // '1undefined'
'1' + null // '1null'

上面代碼須要注意的是字符串與函數的加法運算,會先調用函數的toString()方法,它始終返回當前函數的代碼(看起來就像源代碼通常)。

(function (){}).toString() // 'function (){}'
(function foo(a){ return a + b }).toString() // 'function foo(a){ return a + b }'

因此轉換以後就是'1''function (){}'的拼接操做,結果爲'1function (){}'

ToNumber

JavaScript 遇到預期爲數值的地方,就會將非數值自動轉換爲數值,系統內部會自動調用Number()函數。

除了加法運算符(+)有可能(可能會是字符串拼接的狀況)把自動轉換,其餘運算符都會把非數值自動轉成數值。Number()函數的轉換規則前面已經說過,下面看看幾個例子:

'3' - '2' // 1
'3' * '2' // 6
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'3' * []    // 0
false / '3' // 0
'a' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

上面代碼中須要注意的是,null會被轉換爲0undefined會被轉換爲NaN

NaN,即非數值,是一個特殊的數值,用於表示一個將要返回數值而未返回數值的狀況。關於這個值,須要注意兩點,第一是任何涉及到與 NaN 的運算操做,都會返回 NaN;第二是 NaN 不等於自身(即 NaN === NaN 爲 false),檢測 NaN 須要使用專用的 isNaN 函數。

相等操做符

==在比較時,若是被比較的兩個值類型不一樣,那麼系統會自動轉換成相同類型再比較。

==並非嚴格的比較,只是比較二者的數值。

在比較不一樣的數據類型時,有下面幾種狀況:

  • 若是有一個操做數爲布爾值,這在比較以前先將其轉換爲數值——false轉換爲0true轉換爲1
  • 若是有一個操做數爲字符串,另外一個操做數爲數值,在比較以前先將字符串轉換爲數值。
  • 若是有一個操做數爲對象,另外一個操做數不是,則調用對象的valueOf()方法或toString()方法,用獲得的基本類型值按照起那麼的規則比較。
  • 若是兩個操做符都爲對象,這比較它們是否是同一個對象,若是都指向同一個對象,則爲true(比較引用)
  • 只要有一個操做數爲NaN,一概返回false
  • nullundefined比較,不會進行轉換,直接返回true

    下面看看幾個例子:

    false == 0 // true
    true == 1 // true
    true == 3 // false
    '5' == 5 // true
    var o = {name: 'foo'}
    o == 1 // false
    o == '[object Object]' // true,前面的 o 對象轉換爲基礎類型值 '[object Object]'
    undefined == 0 // false
    null == 0 // false
    null == undefined // true
    'NaN' == NaN // false
    1 == NaN // false
    NaN == NaN // false

在開發時,在遇到不肯定是字符串仍是數值時,可能會使用==來比較,這樣就能夠不用手動強制轉換了。但其實這是偷懶的表現,仍是規規矩矩的先強制轉換後再比較,這樣代碼看上去邏輯會更清晰(若是配置了語法檢測工具eslint,它的建議就是一直使用===而不使用==,避免自動轉換)

toString()方法總結

前面提到了這麼多轉換規則,有不少都是調用的toString()以後才繼續執行的,咱們有必要總結下toString()的返回值。

下面是各類類型的返回狀況:

  • 數值返回當前數值的字符串形式
  • 字符串直接返回
  • 布爾值返回當前值的字符串形式
  • nullundefined沒有toString()方法,通常使用String(),分別返回'null''undefined'
  • 對象返回當前對象的類型字符串(例如'[object Object]''[object Math]')
  • 函數返回當前函數代碼,例如:function foo(){}返回'function foo(){}'(各瀏覽器返回可能有差別)
  • 數組返回每一個成員(會先轉換成字符串)拼接成的字符串,中間以逗號分隔(能夠理解爲調用了join()方法),例如:[1, 2, 3]返回'1,2,3'
  • 日期對象返回當前時區的時間字符串表示,例如:new Date('2018-10-01 12:12:12').toString() 返回'Mon Oct 01 2018 12:12:12 GMT+0800 (中國標準時間)'。但其實在類型轉換中Date類型通常是調用valueOf(),由於它的valueOf()方法返回當前時間的數字值表示(即時間戳),返回結果已是基礎類型值了,不會再調用toString()方法了。

最後,看幾個有意思的類型轉換例子

  1. +new Date()

    這個返回時間戳的方法相信你們都常常用到,這裏+運算符的規則和Number()轉型函數是相同的,對於對象首先會先調用它的valueOf()方法,Date類型的valueOf()方法返回當前時間的數字值表示(即時間戳),返回結果爲數值,不會繼續下一步調用toSting()方法,直接對當前返回值使用Number()仍是返回時間戳

  2. (!+[]+[]+![]).length = 9

    xxx.length,粗略一看有length屬性的就字符串和數組了,因此前面的結果確定是這兩個其中一種。這裏是類型轉換,不可能出現轉換爲數組的狀況,那麼只多是字符串,下面咱們分析一下。

    首先拆開來看,就是!+[][]![]的相加操做

    !+[]會進行以下轉換

    [].toString() // ''
    +'' // 0
    !0 // true

    都轉換完成後,就是true''false的相加操做,結果爲'truefalse',最後結果就是9

  3. [] + {} 和 {} + []

    先說 [] + {} 。一個數組加一個對象。

    []{}valueOf()都返回對象自身,因此都會調用 toString(),最後的結果是字符串串接。[].toString() 返回空字符串,({}).toString() 返回'[object Object]'。最後的結果就是'[object Object]'

    而後說 {} + [] ,看上去應該和上面同樣。可是 {} 除了表示一個對象以外,也能夠表示一個空的block

    在 [] + {} 中,[] 被解析爲數組,所以後續的 + 被解析爲加法運算符,而 {} 就解析爲對象。但在 {} + [] 中,開頭的{} 被解析爲空的 block,隨後的 + 被解析爲正號運算符,最後的結果就是0。即實際上成了:

{} // empty block 
   +[] // 0

相信很多人在遇到類型轉換尤爲是自動轉換的時候,都是靠猜的。如今看了這麼多底層的轉換規則,應該或多或少的對其都有了必定的瞭解了。

相關文章
相關標籤/搜索