# 數據類型的轉換
## 概述
JavaScript 是一種動態類型語言,變量沒有類型限制,可以隨時賦予任意值。
```javascript
var x = y ? 1 : 'a';
```
上面代碼中,變量`x`到底是數值還是字符串,取決于另一個變量`y`的值。`y`為`true`時,`x`是一個數值;`y`為`false`時,`x`是一個字符串。這意味著,`x`的類型沒法在編譯階段就知道,必須等到運行時才能知道。
雖然變量的數據類型是不確定的,但是各種運算符對數據類型是有要求的。如果運算符發現,運算子的類型與預期不符,就會自動轉換類型。比如,減法運算符預期左右兩側的運算子應該是數值,如果不是,就會自動將它們轉為數值。
```javascript
'4' - '3' // 1
```
上面代碼中,雖然是兩個字符串相減,但是依然會得到結果數值`1`,原因就在于 JavaScript 將運算子自動轉為了數值。
本章講解數據類型自動轉換的規則。在此之前,先講解如何手動強制轉換數據類型。
## 強制轉換
強制轉換主要指使用`Number()`、`String()`和`Boolean()`三個函數,手動將各種類型的值,分別轉換成數字、字符串或者布爾值。
### Number()
使用`Number`函數,可以將任意類型的值轉化成數值。
下面分成兩種情況討論,一種是參數是原始類型的值,另一種是參數是對象。
**(1)原始類型值**
原始類型值的轉換規則如下。
```javascript
// 數值:轉換后還是原來的值
Number(324) // 324
// 字符串:如果可以被解析為數值,則轉換為相應的數值
Number('324') // 324
// 字符串:如果不可以被解析為數值,返回 NaN
Number('324abc') // NaN
// 空字符串轉為0
Number('') // 0
// 布爾值:true 轉成 1,false 轉成 0
Number(true) // 1
Number(false) // 0
// undefined:轉成 NaN
Number(undefined) // NaN
// null:轉成0
Number(null) // 0
```
`Number`函數將字符串轉為數值,要比`parseInt`函數嚴格很多。基本上,只要有一個字符無法轉成數值,整個字符串就會被轉為`NaN`。
```javascript
parseInt('42 cats') // 42
Number('42 cats') // NaN
```
上面代碼中,`parseInt`逐個解析字符,而`Number`函數整體轉換字符串的類型。
另外,`parseInt`和`Number`函數都會自動過濾一個字符串前導和后綴的空格。
```javascript
parseInt('\t\v\r12.34\n') // 12
Number('\t\v\r12.34\n') // 12.34
```
**(2)對象**
簡單的規則是,`Number`方法的參數是對象時,將返回`NaN`,除非是包含單個數值的數組。
```javascript
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5
```
之所以會這樣,是因為`Number`背后的轉換規則比較復雜。
第一步,調用對象自身的`valueOf`方法。如果返回原始類型的值,則直接對該值使用`Number`函數,不再進行后續步驟。
第二步,如果`valueOf`方法返回的還是對象,則改為調用對象自身的`toString`方法。如果`toString`方法返回原始類型的值,則對該值使用`Number`函數,不再進行后續步驟。
第三步,如果`toString`方法返回的是對象,就報錯。
請看下面的例子。
```javascript
var obj = {x: 1};
Number(obj) // NaN
// 等同于
if (typeof obj.valueOf() === 'object') {
Number(obj.toString());
} else {
Number(obj.valueOf());
}
```
上面代碼中,`Number`函數將`obj`對象轉為數值。背后發生了一連串的操作,首先調用`obj.valueOf`方法, 結果返回對象本身;于是,繼續調用`obj.toString`方法,這時返回字符串`[object Object]`,對這個字符串使用`Number`函數,得到`NaN`。
默認情況下,對象的`valueOf`方法返回對象本身,所以一般總是會調用`toString`方法,而`toString`方法返回對象的類型字符串(比如`[object Object]`)。所以,會有下面的結果。
```javascript
Number({}) // NaN
```
如果`toString`方法返回的不是原始類型的值,結果就會報錯。
```javascript
var obj = {
valueOf: function () {
return {};
},
toString: function () {
return {};
}
};
Number(obj)
// TypeError: Cannot convert object to primitive value
```
上面代碼的`valueOf`和`toString`方法,返回的都是對象,所以轉成數值時會報錯。
從上例還可以看到,`valueOf`和`toString`方法,都是可以自定義的。
```javascript
Number({
valueOf: function () {
return 2;
}
})
// 2
Number({
toString: function () {
return 3;
}
})
// 3
Number({
valueOf: function () {
return 2;
},
toString: function () {
return 3;
}
})
// 2
```
上面代碼對三個對象使用`Number`函數。第一個對象返回`valueOf`方法的值,第二個對象返回`toString`方法的值,第三個對象表示`valueOf`方法先于`toString`方法執行。
### String()
`String`函數可以將任意類型的值轉化成字符串,轉換規則如下。
**(1)原始類型值**
- **數值**:轉為相應的字符串。
- **字符串**:轉換后還是原來的值。
- **布爾值**:`true`轉為字符串`"true"`,`false`轉為字符串`"false"`。
- **undefined**:轉為字符串`"undefined"`。
- **null**:轉為字符串`"null"`。
```javascript
String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) // "null"
```
**(2)對象**
`String`方法的參數如果是對象,返回一個類型字符串;如果是數組,返回該數組的字符串形式。
```javascript
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"
```
`String`方法背后的轉換規則,與`Number`方法基本相同,只是互換了`valueOf`方法和`toString`方法的執行順序。
1. 先調用對象自身的`toString`方法。如果返回原始類型的值,則對該值使用`String`函數,不再進行以下步驟。
2. 如果`toString`方法返回的是對象,再調用原對象的`valueOf`方法。如果`valueOf`方法返回原始類型的值,則對該值使用`String`函數,不再進行以下步驟。
3. 如果`valueOf`方法返回的是對象,就報錯。
下面是一個例子。
```javascript
String({a: 1})
// "[object Object]"
// 等同于
String({a: 1}.toString())
// "[object Object]"
```
上面代碼先調用對象的`toString`方法,發現返回的是字符串`[object Object]`,就不再調用`valueOf`方法了。
如果`toString`法和`valueOf`方法,返回的都是對象,就會報錯。
```javascript
var obj = {
valueOf: function () {
return {};
},
toString: function () {
return {};
}
};
String(obj)
// TypeError: Cannot convert object to primitive value
```
下面是通過自定義`toString`方法,改變返回值的例子。
```javascript
String({
toString: function () {
return 3;
}
})
// "3"
String({
valueOf: function () {
return 2;
}
})
// "[object Object]"
String({
valueOf: function () {
return 2;
},
toString: function () {
return 3;
}
})
// "3"
```
上面代碼對三個對象使用`String`函數。第一個對象返回`toString`方法的值(數值3),第二個對象返回的還是`toString`方法的值(`[object Object]`),第三個對象表示`toString`方法先于`valueOf`方法執行。
### Boolean()
`Boolean()`函數可以將任意類型的值轉為布爾值。
它的轉換規則相對簡單:除了以下五個值的轉換結果為`false`,其他的值全部為`true`。
- `undefined`
- `null`
- `0`(包含`-0`和`+0`)
- `NaN`
- `''`(空字符串)
```javascript
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
```
當然,`true`和`false`這兩個布爾值不會發生變化。
```javascript
Boolean(true) // true
Boolean(false) // false
```
注意,所有對象(包括空對象)的轉換結果都是`true`,甚至連`false`對應的布爾對象`new Boolean(false)`也是`true`(詳見《原始類型值的包裝對象》一章)。
```javascript
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true
```
所有對象的布爾值都是`true`,這是因為 JavaScript 語言設計的時候,出于性能的考慮,如果對象需要計算才能得到布爾值,對于`obj1 && obj2`這樣的場景,可能會需要較多的計算。為了保證性能,就統一規定,對象的布爾值為`true`。
## 自動轉換
下面介紹自動轉換,它是以強制轉換為基礎的。
遇到以下三種情況時,JavaScript 會自動轉換數據類型,即轉換是自動完成的,用戶不可見。
第一種情況,不同類型的數據互相運算。
```javascript
123 + 'abc' // "123abc"
```
第二種情況,對非布爾值類型的數據求布爾值。
```javascript
if ('abc') {
console.log('hello')
} // "hello"
```
第三種情況,對非數值類型的值使用一元運算符(即`+`和`-`)。
```javascript
+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN
```
自動轉換的規則是這樣的:預期什么類型的值,就調用該類型的轉換函數。比如,某個位置預期為字符串,就調用`String()`函數進行轉換。如果該位置既可以是字符串,也可能是數值,那么默認轉為數值。
由于自動轉換具有不確定性,而且不易除錯,建議在預期為布爾值、數值、字符串的地方,全部使用`Boolean()`、`Number()`和`String()`函數進行顯式轉換。
### 自動轉換為布爾值
JavaScript 遇到預期為布爾值的地方(比如`if`語句的條件部分),就會將非布爾值的參數自動轉換為布爾值。系統內部會自動調用`Boolean()`函數。
因此除了以下五個值,其他都是自動轉為`true`。
- `undefined`
- `null`
- `+0`或`-0`
- `NaN`
- `''`(空字符串)
下面這個例子中,條件部分的每個值都相當于`false`,使用否定運算符后,就變成了`true`。
```javascript
if ( !undefined
&& !null
&& !0
&& !NaN
&& !''
) {
console.log('true');
} // true
```
下面兩種寫法,有時也用于將一個表達式轉為布爾值。它們內部調用的也是`Boolean()`函數。
```javascript
// 寫法一
expression ? true : false
// 寫法二
!! expression
```
### 自動轉換為字符串
JavaScript 遇到預期為字符串的地方,就會將非字符串的值自動轉為字符串。具體規則是,先將復合類型的值轉為原始類型的值,再將原始類型的值轉為字符串。
字符串的自動轉換,主要發生在字符串的加法運算時。當一個值為字符串,另一個值為非字符串,則后者轉為字符串。
```javascript
'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"
```
這種自動轉換很容易出錯。
```javascript
var obj = {
width: '100'
};
obj.width + 20 // "10020"
```
上面代碼中,開發者可能期望返回`120`,但是由于自動轉換,實際上返回了一個字符`10020`。
### 自動轉換為數值
JavaScript 遇到預期為數值的地方,就會將參數值自動轉換為數值。系統內部會自動調用`Number()`函數。
除了加法運算符(`+`)有可能把運算子轉為字符串,其他運算符都會把運算子自動轉成數值。
```javascript
'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 ? // NaN
null + 1 // 1
undefined + 1 // NaN
```
上面代碼中,運算符兩側的運算子,都被轉成了數值。
> 注意:`null`轉為數值時為`0`,而`undefined`轉為數值時為`NaN`。
一元運算符也會把運算子轉成數值。
```javascript
+'abc' // NaN
-'abc' // NaN
+true // 1
-false // 0
```
## 參考鏈接
- Axel Rauschmayer, [What is {} + {} in JavaScript?](http://www.2ality.com/2012/01/object-plus-object.html)
- Axel Rauschmayer, [JavaScript quirk 1: implicit conversion of values](http://www.2ality.com/2013/04/quirk-implicit-conversion.html)
- Benjie Gillam, [Quantum JavaScript?](http://www.benjiegillam.com/2013/06/quantum-javascript/)
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio