從 ECMA 規範看 JavaScript 類型轉換

前言

JavaScript 中的類型轉換一直都是讓前端開發者最頭疼的問題。前陣子,推特上有我的專門發了一張圖說 JavaScript 讓人難以想象。前端

image_1dm5s9qr814dvnsi96laugvg9.png-51.4kB

除了這個,還有不少經典的、讓 JavaScript 開發者摸不着頭腦的類型轉換,譬以下面這些,你是否知道結果都是多少?面試

1 + {} === ?
{} + 1 === ?
1 + [] === ?
1 + '2' === ?

本文將帶領你從 ECMA 規範開始,去深刻理解 JavaScript 中的類型轉換,讓類型轉換再也不成爲前端開發中的攔路虎。函數

數據類型

JS 中有六種簡單數據類型:undefinednullbooleanstringnumbersymbol,以及一種複雜類型:object
可是 JavaScript 在聲明時只有一種類型,只有到運行期間纔會肯定當前類型。在運行期間,因爲 JavaScript 沒有對類型作嚴格限制,致使不一樣類型之間能夠進行運算,這樣就須要容許類型之間互相轉換。spa

類型轉換

顯式類型轉換

顯式類型轉換就是手動地將一種值轉換爲另外一種值。通常來講,顯式類型轉換也是嚴格按照上面的表格來進行類型轉換的。prototype

經常使用的顯式類型轉換方法有 NumberStringBooleanparseIntparseFloattoString 等等。
這裏須要注意一下 parseInt,有一道題偶爾會在面試中遇到。翻譯

問:爲何 [1, 2, 3].map(parseInt) 返回 [1,NaN,NaN]?
答:parseInt函數的第二個參數表示要解析的數字的基數。該值介於 2 ~ 36 之間。

若是省略該參數或其值爲 0,則數字將以 10 爲基礎來解析。若是它以 「0x」 或 「0X」 開頭,將以 16 爲基數。3d

若是該參數小於 2 或者大於 36,則 parseInt() 將返回 NaN。
通常來講,類型轉換主要是基本類型轉基本類型、複雜類型轉基本類型兩種。
轉換的目標類型主要分爲如下幾種:code

  1. 轉換爲 string
  2. 轉換爲 number
  3. 轉換爲 boolean

我參考了 ECMA-262 的官方文檔來總結一下這幾種類型轉換。ECMA 文檔連接:ECMA-262對象

ToNumber

其餘類型轉換到 number 類型的規則見下方表格:blog

原始值 轉換結果
Undefined NaN
Null 0
true 1
false 0
String 根據語法和轉換規則來轉換
Symbol Throw a TypeError exception
Object 先調用toPrimitive,再調用toNumber

String 轉換爲 Number 類型的規則:

  1. 若是字符串中只包含數字,那麼就轉換爲對應的數字。
  2. 若是字符串中只包含十六進制格式,那麼就轉換爲對應的十進制數字。
  3. 若是字符串爲空,那麼轉換爲0。
  4. 若是字符串包含上述以外的字符,那麼轉換爲 NaN。

使用+能夠將其餘類型轉爲 number 類型,咱們用下面的例子來驗證一下。

+undefined // NaN
+null // 0
+true // 1
+false // 0
+'111' // 111
+'0x100F' // 4111
+'' // 0
'b' + 'a' + + 'a' + 'a' // 'baNaNa'
+Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number

ToBoolean

原始值 轉換結果
Undefined false
Boolean true or false
Number 0和NaN返回false,其餘返回true
Symbol true
Object true

咱們也可使用 Boolean 構造函數來手動將其餘類型轉爲 boolean 類型。

Boolean(undefined) // false
Boolean(1) // true
Boolean(0) // false
Boolean(NaN) // false
Boolean(Symbol()) // true
Boolean({}) // true

ToString

原始值 轉換結果
Undefined 'Undefined'
Boolean 'true' or 'false'
Number 對應的字符串類型
String String
Symbol Throw a TypeError exception
Object 先調用toPrimitive,再調用toNumber

轉換到 string 類型能夠用模板字符串來實現。

`${undefined}` // 'undefined'
`${true}` // 'true'
`${false}` // 'false'
`${11}` // '11'
`${Symbol()}` // Cannot convert a Symbol value to a string
`${{}}`

隱式類型轉換

隱式類型轉換通常是在涉及到運算符的時候纔會出現的狀況,好比咱們將兩個變量相加,或者比較兩個變量是否相等。
隱式類型轉換其實在咱們上面的例子中已經有所體現。對於對象轉原始類型的轉換,也會遵照 ToPrimitive 的規則,下面會進行細說。

從ES規範來看類型轉換

ToPrimitive

在對象轉原始類型的時候,通常會調用內置的 ToPrimitive 方法,而 ToPrimitive 方法則會調用 OrdinaryToPrimitive 方法,咱們能夠看一下 ECMA 的官方文檔。

image_1dard6av87ir24p140nv5d1vq9.png-182.5kB

我來翻譯一下這段話。

ToPrimitive 方法接受兩個參數,一個是輸入的值 input,一個是指望轉換的類型 PreferredType

  1. 若是沒有傳入 PreferredType 參數,讓 hint 等於"default"
  2. 若是 PreferredTypehint String,讓 hint 等於"string"
  3. 若是 PreferredTypehint Number,讓 hint 等於"number"
  4. exoticToPrim 等於 GetMethod(input, @@toPrimitive),意思就是獲取參數 input@@toPrimitive 方法
  5. 若是 exoticToPrim 不是 Undefined,那麼就讓 result 等於 Call(exoticToPrim, input, « hint »),意思就是執行 exoticToPrim(hint),若是執行後的結果 result 是原始數據類型,返回 result,不然就拋出類型錯誤的異常
  6. 若是 hint 是"default",讓 hint 等於"number"
  7. 返回 OrdinaryToPrimitive(input, hint) 抽象操做的結果

OrdinaryToPrimitive

OrdinaryToPrimitive 方法也接受兩個參數,一個是輸入的值O,一個也是指望轉換的類型 hint

  1. 若是輸入的值是個對象
  2. 若是 hint 是個字符串而且值爲'string'或者'number'
  3. 若是 hint 是'string',那麼就將 methodNames 設置爲 toStringvalueOf
  4. 若是 hint 是'number',那麼就將 methodNames 設置爲 valueOftoString
  5. 遍歷 methodNames 拿到當前循環中的值 name,將 method 設置爲 O[name](即拿到 valueOftoString 兩個方法)
  6. 若是 method 能夠被調用,那麼就讓 result 等於 method 執行後的結果,若是 result 不是對象就返回 result,不然就拋出一個類型錯誤的報錯。

ToPrimitive 的代碼實現

若是隻用文字來描述,你確定會以爲過於晦澀難懂,因此這裏我就本身用代碼來實現這兩個方法幫助你的理解。

// 獲取類型
const getType = (obj) => {
    return Object.prototype.toString.call(obj).slice(8,-1);
}
// 是否爲原始類型
const isPrimitive = (obj) => {
    const types = ['String','Undefined','Null','Boolean','Number'];
      return types.indexOf(getType(obj)) !== -1;
}
const ToPrimitive = (input, preferredType) => {
    // 若是input是原始類型,那麼不須要轉換,直接返回
    if (isPrimitive(input)) {
        return input;
    }
    let hint = '', 
        exoticToPrim = null,
        methodNames = [];
    // 當沒有提供可選參數preferredType的時候,hint會默認爲"default";
    if (!preferredType) {
        hint = 'default'
    } else if (preferredType === 'string') {
        hint = 'string'
    } else if (preferredType === 'number') {
        hint = 'number'
    }
    exoticToPrim = input.@@toPrimitive;
    // 若是有toPrimitive方法
    if (exoticToPrim) {
        // 若是exoticToPrim執行後返回的是原始類型
        if (typeof (result = exoticToPrim.call(O, hint)) !== 'object') {
            return result;
        // 若是exoticToPrim執行後返回的是object類型
        } else {
            throw new TypeError('TypeError exception')
        }
    }
    // 這裏給了默認hint值爲number,Symbol和Date經過定義@@toPrimitive方法來修改默認值
    if (hint === 'default') {
        hint = 'number'
    }
    return OrdinaryToPrimitive(input, hint)
}
const OrdinaryToPrimitive = (O, hint) => {
    let methodNames = null,
        result = null;
    if (typeof O !== 'object') {
        return;
    }
    // 這裏決定了先調用toString仍是valueOf
    if (hint === 'string') {
        methodNames = [input.toString, input.valueOf]
    } else {
        methodNames = [input.valueOf, input.toString]
    }
    for (let name in methodNames) {
        if (O[name]) {
            result = O[name]()
            if (typeof result !== 'object') {
                return result
            }
        }
    }
    throw new TypeError('TypeError exception')
}

總結一下,在進行類型轉換的時候,通常是經過 ToPrimitive 方法將引用類型轉爲原始類型。若是引用類型上有 @@toPrimitive 方法,就調用 @@toPrimitive 方法,執行後的返回值爲原始類型就直接返回,若是依然是對象,那麼就拋出報錯。

若是對象上沒有 toPrimitive 方法,那麼就根據轉換的目標類型來判斷先調用 toString 仍是 valueOf 方法,若是執行這兩個方法後獲得了原始類型的值,那麼就返回。不然,將會拋出錯誤。

Symbol.toPrimitive

在 ES6 以後提供了 Symbol.toPrimitive 方法,該方法在類型轉換的時候優先級最高。

const obj = {
  toString() {
    return '1111'
  },
  valueOf() {
    return 222
  },
  [Symbol.toPrimitive]() {
    return 666
  }
}
const num = 1 + obj; // 667
const str = '1' + obj; // '1666'

例子

也許上面關於 ToPrimitive 的代碼講解你仍是會以爲晦澀難懂,那我接下來就舉幾個例子來講明對象的類型轉換。

var a = 1, 
    b = '2';
var c = a + b; // '12'

也許你會好奇,爲何不是將後面的 b 轉換爲 number 類型,最後獲得3?
咱們仍是要先看文檔對加號的定義。

image_1davvk6ij3lnsisjsk1i8djf8p.png-243.3kB

首先會分別執行兩個值的 toPrimitive 方法,由於 ab 都是原始類型,因此仍是獲得了1和'2'。
從圖上看到若是轉換後的兩個值的 Type 有一個是 String 類型,那麼就將兩個值通過 toString 轉換後串起來。所以最後獲得了'12',而不是3。

咱們還能夠再看一個例子。

var a = 'hello ', b = {};
var c = a + b; // "hello [object Object]"

這裏還會分別執行兩個值的 toPrimitive 方法,a 仍是獲得了'hello ',而b因爲沒有指定preferredType,因此會默認被轉爲 number 類型,先調用 valueOf,但 valueOf 仍是返回了一個空對象,不是原始類型,因此再調用 toString,獲得了 '[object Object]',最後將二者鏈接起來就成了 "hello [object Object]"
若是咱們想返回 'hello world',那該怎麼改呢?只須要修改 bvalueOf 方法就行了。

b.valueOf = function() {
    return 'world'
}
var c = a + b; // 'hello world'

也許你在面試題中看到過這個例子。

var a = [], b = [];
var c = a + b; // ''

這裏爲何 c 最後是''呢?由於 ab 在執行 valueOf 以後,獲得的依然是個 [] ,這並不是原始類型,所以會繼續執行 toString,最後獲得'',兩個''相加又獲得了''。
咱們再看一個指定了 preferredType 的例子。

var a = [1, 2, 3], b = {
    [a]: 111
}

因爲 a 是做爲了 b 的鍵值,因此 preferredTypestring,這時會調用 a.toString 方法,最後獲得了'1,2,3'

總結

類型轉換一直是學 JS 的時候很難搞明白的一個概念,由於轉換規則比較複雜,常常讓人以爲莫名其妙。可是若是從 ECMA 的規範去理解這些轉換規則的原理,那麼就會很容易知道爲何最後會獲得那些結果。

相關文章
相關標籤/搜索