淺談JavaScript的類型轉換

這些是本人在 github.pages上寫的博客,歡迎你們關注和糾錯,本人會按期在github pages上更新。有想要深刻了解的知識點能夠留言。git

概述

在 JavaScript 中,將一種值類型轉換爲另外一種值類型,叫作類型轉換,出於動態型語言的特性,類型轉換髮生在運行時階段。這些轉換在咱們平時寫的代碼裏無處不在,儘管咱們沒有注意,可是這些轉換已經存在於咱們的代碼裏了。像 if、for、while、==、===、+、- 等等語句中。github

而在 JavaScript 中,有兩種轉換風格:隱式強制類型轉換和顯式強制類型轉換。安全

舉個栗子bash

let str = 42 + '' // '42'  隱式
    let anotherStr = String(42) // '42'  顯示

複製代碼

下面,將會從類型轉換的運行機制對轉換值的機制進行深刻分析oop

值轉換的抽象操做

下面介紹一些抽象操做,ToString, ToNumber, ToBoolean, ToPrimitive 注意:這裏的抽象操做不表明方法,而是對類型進行轉換執行的一系列方法。性能

ToString

ToString 主要負責處理非字符串類型轉換爲字符串類型。咱們將待轉換的類型進行劃分:學習

基本類型和對象類型ui

string 類型是 JS 中很特殊,也是最重要的基本類型,基本每一個內置對象都實現了自身的 toString 方法。spa

基本類型值的操做很常規,都遵循着通用的規則。prototype

null -> 'null'
    undefined -> 'undefined'
    true -> 'true'
    21 -> '21'

複製代碼

對普通的對象而言,機制就變得複雜起來。

對象自身調用 toString() 進行字符串化或者顯示字符串化(如: String() )。

基本上 obj.toString() === String(obj) 過程以下:

  • 先檢查該對象是否有 toString 方法。若是該對象自身重寫了從 Object 原型鏈繼承的 toString ,那麼調用該重寫的方法
  • 若是該對象自身沒有 toString 方法。那麼就調用原型鏈上游的 toString 方法,直到調用到 Object.prototype.toString() 方法。若是原型鏈上沒有此方法或者 toString 方法被重寫爲普通屬性,則拋出 TypeError 的錯誤
let arr = [1, 2, 3]
    arr.toString() === String(arr) // '1,2,3'
    let obj = { name: 'jack' }
    obj.toString() === String(obj) // [object, Object]

複製代碼

特殊狀況:調用 String(obj) 時,若是對象的 toString 方法被重寫爲普通屬性,則會退而求其次執行 valueOf 方法。若是執行的結果未返回基本類型,則會報錯。

let obj = {
        valueOf() {
            return '12'
        }
        toString: undefined
    }
    String(obj) // 12
    obj.valueOf = () => ({})
    String(obj) // TypeError:Cannot convert object to primitive value

複製代碼

建議:不要輕易的重寫對象屬性的 valueOf 和 toString 方法。由於這兩個屬性涉及對象的表現形式。在類型轉換中相當重要。

Object.prototype.toString() 方法返回的是內部屬性 [[ class ]] 的值。如([objecct, Object], [object, Function])

ToPrimitive

該抽象操做爲了將對象類型轉換爲基本類型,步驟以下:

  • 檢查該值是否有 valueOf() 方法。若是有且返回基本類型值,則使用該值
  • 若是沒有返回基本類型值或者沒有該方法,就使用 toString 方法的返回值(若是存在)
  • 若是 valueOf 或者 toString 都不返回基本類型值 則會報錯 TypeError
let nullObj = Object.create(null)
    Number(nullObj) || String(nullObj) // VM3789:1 Uncaught TypeError: Cannot convert object to primitive value

    let obj = {
        valueOf: function() {
            return '12'
        }
    }

    Number(obj) // 12 通過的步驟 valueOf -> '12' -> 通過字符串轉數字 -> 12
    Number([]) // 0 通過步驟 valueOf -> [] -> toString -> '' -> 通過字符串轉數字 -> 0

複製代碼

ToNumber

ToNumber 主要負責將其餘類型轉換爲 number 類型

處理規則:

  • true 轉換爲 1,false 轉換爲 0
  • undefined 轉換爲 NaN
  • null 轉換爲 0
  • "" 轉換爲 0,其餘字符串轉換按照特定規則解析 若是能將其解析成數字類型,就解析成該值,不然爲NaN
  • 對象類型先將其轉換爲基本類型( ToPrimitive ),而後再按上述步驟進行轉換。
Number('') // 0
    Number(true) // 1
    Number(false) // 0
    Number(undefined) // NaN
    Number(null) // 0
    Number('1d1') // NaN
    let obj = {
        valueOf() {
            return '142'
        }
        toString() {
            return 'obj self'
        }
    }
    Number(obj) // 經歷過程 obj 先通過ToPrimitive抽象操做 valueOf -> '142' ->ToNumber -> 142
    obj.valueOf = null // 這個時候 valueOf 不是一個方法,因此直接調用 toString()
    Number(obj) // ToPrimitive 無 valueOf 方法 -> toString() -> NaN

複製代碼

ToBoolean

假植(falsy)和真值,在 JavaScript 中,除了 true 和 false,還有其餘一些列的真值和假值,這些真值和假植有區別於 true 和 false 總的來講,JavaScript中的值分爲兩類,真值和假值。由於 boolean 類型只有兩個值,沒有第三個值。

下面的這些值是假值,假值的布爾類型轉換爲false

  • false
  • undefined
  • null
  • 0,-0,+0,NaN
  • ""

除了以上列出的假值,其餘全爲真值。因此ToBoolean的操做也很簡單。就是尋找以上假值中是否存在目標值。 存在即爲false,不存在即爲true

顯式強制類型轉換

數字、字符串和布爾值的強制類型轉換

規則以下:

  • 數字的強制類型轉換遵循以上所述的 ToNumber 抽象操做,一般所用方法是 Number(), 一元操做符+
  • 字符串的強制類型轉換遵循以上所述的 ToString 抽象操做,一般所用方法是 String(), toString()
  • 布爾值的強制類型轉換遵循以上所述的 ToBoolean 抽象操做,一般全部方法是 Boolean(), !!val
Number('12') // 12
    +'12' // 12
    Number('') // 0
    +'' // 0
    Number({}) // NaN
    Number([]) // 0
    String(12) // '12'
    String({}) // '[object Object]'
    String([]) // ''
    Boolean('') // false
    Boolean(0) // false
    Boolean(1) // true
    !!1 // true
    !1 // false
    [1,2,'',undefined,0].filter(Boolean) -> [1,2]
    [1,3,'',undefined,0].filter(v -> !!v) -> [1,2]

複製代碼

詳解 parseInt 方法

parseInt 方法區別於 Number 方法。不一樣點有

  • parseInt 只處理字符串類型,若是接受的參數不是字符串類型,會先將其轉化爲字符串類型(執行 ToString 抽象操做)。
  • parseInt 支持轉化爲特定的數據類型,第二個參數就是特定的進制數據類型。第二個參數默認值爲10,即十進制解析
  • parseInt 能夠會從前至後依次按照第二個參數進制解析字符串,一旦解析到爲 NaN 時。則結束解析。返回以前解析的結果。

注意:parseInt 要解析的參數,若是第一個參數以0開頭,會按照八進制數據進行解析。0x 會按照十六進制進行解析。會覆蓋默認的十進制解析。即便顯式的指定十進制解析,也會進行覆蓋。

parseInt('112ssasd') // 112
    parseInt(true) // NaN
    parseInt(null) // NaN
    parseInt(112, 2) // 3 解析過程 112 按照二進制解析 二進制只能識別 0 和 1,因此 只能解析 11,二進制的結果爲3
    parseInt(012) // 10
    parseInt(0x12) // 18

複製代碼

常見的考題: [ 1,2,4 ].map(parseInt) 的結果,根據上述分析,顯然不是[ 1,2,4 ]了。 下面的幾種結果,停下來,思考一下,本身能夠分析出來,也就對該方法完全掌握了

let noop = function() { }
    parseInt(noop, 15)
    parseInt(noop, 16)
    parseInt(1/0)
    parseInt(1/0, 19)

複製代碼

隱式強制類型轉換

字符串的隱式類型轉換

二元運算符 + 是最重要的一個操做符,由於該運算符便可以做爲兩個數字進行相加,又能夠做爲字符串的鏈接符號。 如今,咱們討論做爲鏈接符時的注意點以及相關規則。

規則:

先將 + 兩側的數據類型轉化爲基本數據類型(ToPrimitive 抽象操做),若是一邊有字符串,那麼此時,就做爲鏈接符使用。

注意點:

  • 是將對象運用 ToPrimitive 抽象操做, 即先 valueOf ,而後 toString (若是有必要)
  • 有區別於 String(): String() 是直接尋找 toString 方法,若是未找到,尋找原型鏈上的方法。若是當前 toString 未被定義爲方法,則調用valueOf。若是未能轉化爲基本類型,則報錯typeError。
let num = 1
    num + '' // '1'
    let bool = true
    bool + '' // 'true'
    let nul = null
    nul + '' // 'nul'
    let arr = [ 1,2,3 ]
    arr + '' // '1,2,3'
    let obj = {}
    obj + '' // '[object Object]'
    arr.valueOf = () => '111'
    arr + '' // '111'
    arr + 1 // '1111'
    1 + [1] // '11'

複製代碼

數字的隱式類型轉換

數字的隱式轉換有多種,像二元操做符 + - * / % 均可以對類型進行數字的隱式類型轉換,咱們先來討論特殊的 +

  • 二元操做符 + 若是有非基本類型的值,先轉化爲基本類型的值(ToPrimitive抽象操做)
  • 若是沒有出現字符串,則使用數字相加進行運算,非數字類型參照上述規則 ToNumber 抽象操做轉換。
null + 1 // 1
    true + 1 // 2
    false + 1 // 1
    undefined + 1 // NaN
    let obj = {
        valueOf: function() {
            return 12
        }
    }
    obj + 1 // 13

複製代碼

而其餘的二元操做符 如: -、*、%、/ 都會對操做符兩側進行 ToNumber 抽象操做。至關於 Number(variable)

1 - '12' // -11 至關於 1 - Number('12')
    1 * '12' // 12
    1 - [1] // 0
    1 / '12' // 0.08333333333333333
    1 % '12' // 1

複製代碼

布爾值的隱式類型轉換

布爾值的隱式類型轉換規則很簡單 參照上述的 ToBoolean 的抽象操做就能夠了,除去列出的假值,其餘均爲真值

那麼布爾值的應用場景有哪些呢?

  • 除去咱們經常使用的 if 語句等循環判斷語句以外。有filter
  • 還有咱們以後要討論的邏輯運算符
以前的寫法是顯示的轉換 如:
    let arr = [1, 2, 4, '', 0, undefined, null, [], {}]
    arr.filter(Boolean) // [1, 2, 4, [], {}]
    arr.filter(v => !!v) // [1, 2, 4, [], {}]
    如今咱們能夠寫成隱式轉換
    arr.filter(v => v) // [1, 2, 4, [], {}]

複製代碼

|| 和 && 的抽象邏輯

不少人都認爲 || 和 && 是返回布爾值的,這是一種誤解。其實這兩個運算符歷來都不是用來返回布爾值的,相反,這是一種運算,能夠返回任意類型的值。 接下來詳細介紹這兩個運算符。

|| 的使用方式 如 a || b ,運算規則以下

先對 a 進行抽象類型轉換 (ToBoolean抽象操做),若是 a 是真值,那麼直接返回 a ,不然返回 b

let a = [], b = ''
    a || b // [] 由於 a 是真值,因此直接返回 a,不對 b 進行計算。
    b || a // [] 由於 b 是假值,因此直接返回 a

複製代碼

&& 的使用方式 如 a && b,運算規則以下

先對 a 進行抽象類型轉換(ToBoolean抽象操做),若是 a 是真值,那麼直接返回 b,不然返回 a

let a = [], b = ''
    a && b // '' 由於 a 是真值,因此直接返回 b
    b && a // '' 由於 b 是假值,因此直接返回 b,不對 a 進行計算

複製代碼

因此 這兩種運算符的性質決定了其又叫短路運算符

抽象相等(==)和 (===)

這兩個操做符,咱們平時在寫代碼時,用到的地方特別多,可是如何抉擇?爲何推薦使用全等 ===,而不推薦使用 == ? 咱們都知道 == 是通過類型轉換以後再比較值是否相等。那麼值轉換究竟是什麼樣的順序?

下面將經過深刻分析,爲何 === 比 == 要好。首先咱們須要熟悉規則是什麼?

== 的規則以下:

  • 對象與對象之間進行比較時,結果直接爲false(前提是引用地址的內存地址不一樣)。
  • null 和 undefined 比較結果爲true。與其餘相比直接爲false。
  • 非null 和 非 undefined 狀況下,先將兩邊都轉換爲基本類型( 可能會通過ToPrimitive抽象操做 ),若是兩邊的數據類型都相同,則直接進行比較。
  • 兩邊的數據類型不相同的狀況下,則對兩邊都進行 ToNumber 抽象操做。而後進行比較。

根據ES5規範 11.9.3.2-3規定。 null == undefined

let arr = []
    arr == false // true 轉換過程 [] -> ToPrimitive -> ''(這步還熟悉吧), 兩邊都 ToNumber '' -> 0, false -> 0
    arr == 0 // true
    1 == true // true
    2 == true // false 由於 true -> ToNumber -> 1 至關於 2 == 1 爲false
    [1] == 1 // true
    [1] == [1] // false 
    [] == {} // false
    {} == [] // 會報 SyntaxError,爲何?

複製代碼

針對上述的 {} == [] ,這裏 JS 引擎會將 == 前面的大括號解析爲塊級做用域。因此會報語法錯誤

至關於 
    {
        // some code 
    }
    == []

複製代碼

因此改爲 ({}) == [] 就能夠查看結果了。

比較少見的狀況:

  • 對象重寫了 valueOf 方法,會返回意想不到的值。
  • [] == ![] 結果爲 true。由於 ![] 會先進行 ToBoolean 抽象操做。結果就是 [] == false 很明顯爲true
  • 0 == [ null ] 結果爲 true。由於 [ null ].valueOf() == [ null ] [ null ].toString() == '',因此結果也很明顯

建議

  • 若是兩邊的值有 true 或者 false,避免使用 ==
  • 若是兩邊的值有 []、""、0,避免使用 ==
  • 咱們應該在必要和保證安全的狀況下使用顯式的強制類型轉換來保證程序的可靠性與可讀性

總結:我的來看,抽象相等 == 用的好的話能夠進行不少有趣的代碼組合,前提是類型之間的互相切換咱們已經很熟悉了。可是每次比較均可能會形成兩側的數據進行屢次數據類型轉換。性能和安全性,穩定性都不如 嚴格抽象全等 === 來的高。

抽象關係比較

最後,咱們再簡單介紹一下,> 、< 、>= 、<= 這幾種狀況。

先介紹下規則:

  • 先對操做符的兩邊進行 ToPrimitive 抽象操做(若是有必要的話)
  • 若是結果出現非字符串,那麼將兩邊進行 ToNumber 抽象操做
  • 若是結果兩邊都是字符串類型,則按照字母順序進行比較
// 兩邊出現非字符串
    let arr = [12]
    arr < true // false
    arr < 13 // true
    // 兩邊出現字符串
    '042' < '12' // true
    let anotherArr = [042]
    let temp = [12]
    anotherArr < temp // false 爲何結果爲false '042' < '12' 不是爲 true 嗎 ? 本身思考下 會得出答案的
    let obj = {}
    let obj1 = {}
    obj < obj1 // false 由於 '[object Object]' === '[object Object]'

複製代碼

最後咱們來討論下 a <= b的狀況。

舉個栗子

let obj = {}
    let obj1 = {}
    obj < obj1 // false
    obj == obj1 // false
    obj > obj1 // false
    // 上述3個的結果應該是沒有任何問題
    obj <= obj1 // true ???
    obj >= obj1 // true ???

複製代碼

意想不到的事情發生了是吧,這不是程序執行的問題,這個結果正符合規範的要求

  • 實際在 JavaScript 中, a <= b 會被執行爲 !(a > b)
  • a >= b 同理爲 b <= a。被執行爲 !(b > a)

咱們看剛纔的例子

obj <= obj1 會被執行爲 !(obj > obj1) -> !false -> true
    obj >= obj1 會被執行爲 !(obj < obj1) -> !false -> true

複製代碼

這樣,結果就很順其天然了吧。

總結

今天介紹的類型轉換,不少知識點都是參考 KYLE SIMPSON 著有的 YOU DONT KNOW JAVASCRIPT一書。部分知識參照 ES5規範。 而後根據平常的開發,嘗試作的總結。類型轉換基本就介紹完了。介紹這一知識的目的不是讓咱們在開發中寫出這些生澀的代碼,而是讓咱們透過寫的代碼,理解其運行的本質,這樣,能讓咱們寫出更好的代碼。咱們在學習的過程當中,更加應該,知其然知其因此然。這樣,咱們寫出來的代碼纔會又更高的可讀性和穩定性。

若有理解錯誤或者表達不清楚的地方,歡迎一塊兒交流。

相關文章
相關標籤/搜索