加號+運算符
在 JavaScript 中,加法的規則其實很簡單,只有兩種情況:
- 把數字和數字相加
- 把字符串和字符串相加
所有其他類型的值都會被自動轉換成這兩種類型的值。 為了能夠弄明白這種隱式轉換是如何進行的,我們首先需要搞懂一些基礎知識。
讓我們快速的復習一下。 在 JavaScript 中,一共有兩種類型的值:
- 原始值(primitives)
- undefined
- null
- boolean
- number
- string
- 對象值(objects):除了原始值外,其他的所有值都是對象類型的值,包括數組(array)和函數(function)。
類型轉換
加法運算符會觸發三種類型轉換:
- 轉換為原始值
- 轉換為數字
- 轉換為字符串
通過 ToPrimitive() 將值轉換為原始值
JavaScript 引擎內部的抽象操作 ToPrimitive() 有着這樣的簽名:
ToPrimitive(input,PreferredType?)
可選參數 PreferredType 可以是 Number 或者 String。 它只代表了一個轉換的偏好,轉換結果不一定必須是這個參數所指的類型(汗),但轉換結果一定是一個原始值。 如果 PreferredType 被標志為 Number,則會進行下面的操作來轉換input
- 如果 input 是個原始值,則直接返回它。
- 否則,如果 input 是一個對象。則調用 obj.valueOf() 方法。 如果返回值是一個原始值,則返回這個原始值。
- 否則,調用 obj.toString() 方法。 如果返回值是一個原始值,則返回這個原始值。
- 否則,拋出 TypeError 異常。
如果 PreferredType 被標志為 String,則轉換操作的第二步和第三步的順序會調換。 如果沒有 PreferredType 這個參數,則 PreferredType 的值會按照這樣的規則來自動設置:
- Date 類型的對象會被設置為 String
- 其它類型的值會被設置為 Number
通過 ToNumber() 將值轉換為數字
下面的表格解釋了 ToNumber() 是如何將原始值轉換成數字的
參數 | 結果 |
---|---|
undefined | NaN |
null | +0 |
boolean | true被轉換為1,false轉換為+0 |
number | 無需轉換 |
string | 由字符串解析為數字。例如,"324"被轉換為324 |
如果輸入的值是一個對象,則會首先會調用 ToPrimitive(obj, Number) 將該對象轉換為原始值, 然后在調用 ToNumber() 將這個原始值轉換為數字。
通過ToString()將值轉換為字符串
下面的表格解釋了 ToString() 是如何將原始值轉換成字符串的
參數 | 結果 |
---|---|
undefined | "undefined" |
null | "null" |
boolean | "true" 或者 "false" |
number | 數字作為字符串。比如,"1.765" |
string | 無需轉換 |
如果輸入的值是一個對象,則會首先會調用 ToPrimitive(obj, String) 將該對象轉換為原始值, 然后再調用 ToString() 將這個原始值轉換為字符串。
實踐一下
下面的對象可以讓你看到引擎內部的轉換過程。
var obj = { valueOf: function () { console.log("valueOf"); return {}; // not a primitive }, toString: function () { console.log("toString"); return {}; // not a primitive } }
Number 作為一個函數被調用(而不是作為構造函數調用)時,會在引擎內部調用 ToNumber() 操作:
> Number(obj) valueOf toString TypeError: Cannot convert object to primitive value
加法
有下面這樣的一個加法操作。
value1 + value2
在計算這個表達式時,內部的操作步驟是這樣的
- 將兩個操作數轉換為原始值 (以下是數學表示法的偽代碼,不是可以運行的 JavaScript 代碼):
prim1 := ToPrimitive(value1) prim2 := ToPrimitive(value2)
PreferredType 被省略,因此 Date 類型的值采用 String,其他類型的值采用 Number。
- 如果 prim1 或者 prim2 中的任意一個為字符串,則將另外一個也轉換成字符串,然后返回兩個字符串連接操作后的結果。
- 否則,將 prim1 和 prim2 都轉換為數字類型,返回他們的和。
下面的表格就是 + 運算符對於不同類型進行運算后,得到的結果類型
---------------------------------------------------------------------------------------- | undefined | boolean | number | string | function | object | null | array ---------------------------------------------------------------------------------------- undefined | number | number | number | string | string | string | number | string boolean | number | number | number | string | string | string | number | string number | number | number | number | string | string | string | number | string string | string | string | string | string | string | string | string | string function | string | string | string | string | string | string | string | string object | string | string | string | string | string | string | string | string null | number | number | number | string | string | string | number | string array | string | string | string | string | string | string | string | string -------------------------------------------------------------------------------------------
本表適用於 Chrome 13, Firefox 6, Opera 11 and IE9。
加法的示例
預料到的結果
當你將兩個數組相加時,結果正是我們期望的:
> [] + [] ''
[] 被轉換成一個原始值:首先嘗試 valueOf() 方法,該方法返回數組本身(this):
> var arr = []; > arr.valueOf() === arr true
此時結果不是原始值,所以再調用 toString() 方法,返回一個空字符串(string 是原始值)。 因此,[] + [] 的結果實際上是兩個空字符串的連接。
將一個數組和一個對象相加,結果依然符合我們的期望:
> [] + {} '[object Object]'
解析:將空對象轉換成字符串時,產生如下結果。
> String({}) '[object Object]'
所以最終的結果其實是把 "" 和 "[object Object]" 兩個字符串連接起來。
更多的對象轉換為原始值的例子:
> 5 + new Number(7) 12 > 6 + { valueOf: function () { return 2 } } 8 > "abc" + { toString: function () { return "def" } } 'abcdef'
意想不到的結果
如果 + 加法運算的第一個操作數是個空對象字面量,則會出現詭異的結果(Firefox console 中的運行結果):
> {} + {} NaN
這個問題的原因是,JavaScript 把第一個 {} 解釋成了一個空的代碼塊(code block)並忽略了它。 NaN 其實是表達式 +{} 計算的結果 (+ 加號以及第二個 {})。 你在這里看到的 + 加號並不是二元運算符「加法」,而是一個一元運算符,作用是將它后面的操作數轉換成數字,和 Number() 函數完全一樣。例如:
> +"3.65" 3.65
以下的表達式是它的等價形式:
+{} Number({}) Number({}.toString()) // {}.valueOf() isn’t primitive Number("[object Object]") NaN
為什么第一個 {} 會被解析成代碼塊(code block)呢? 因為整個輸入被解析成了一個語句:如果左大括號出現在一條語句的開頭,則這個左大括號會被解析成一個代碼塊的開始。 所以,你也可以通過強制把輸入解析成一個表達式來修復這樣的計算結果: (譯注:我們期待它是個表達式,結果卻被解析成了語句)
> ({} + {}) '[object Object][object Object]'
一個函數或方法的參數也會被解析成一個表達式:
> console.log({} + {}) [object Object][object Object]
經過前面的講解,對於下面這樣的計算結果,你也應該不會感到吃驚了:
> {} + [] 0
在解釋一次,上面的輸入被解析成了一個代碼塊后跟一個表達式 +[]。 轉換的步驟是這樣的:
+[] Number([]) Number([].toString()) // [].valueOf() isn’t primitive Number("") 0
有趣的是,Node.js 的 REPL 在解析類似的輸入時,與 Firefox 和 Chrome(和Node.js 一樣使用 V8 引擎) 的解析結果不同。 下面的輸入會被解析成一個表達式,結果更符合我們的預料:
> {} + {} '[object Object][object Object]' > {} + [] '[object Object]'