JS中的加號+運算符詳解


加號+運算符

在 JavaScript 中,加法的規則其實很簡單,只有兩種情況:

  • 把數字和數字相加
  • 把字符串和字符串相加

所有其他類型的值都會被自動轉換成這兩種類型的值。 為了能夠弄明白這種隱式轉換是如何進行的,我們首先需要搞懂一些基礎知識。

讓我們快速的復習一下。 在 JavaScript 中,一共有兩種類型的值:

  • 原始值(primitives)
    1. undefined
    2. null
    3. boolean
    4. number
    5. string
  • 對象值(objects):除了原始值外,其他的所有值都是對象類型的值,包括數組(array)和函數(function)。

類型轉換

加法運算符會觸發三種類型轉換:

  1. 轉換為原始值
  2. 轉換為數字
  3. 轉換為字符串

通過 ToPrimitive() 將值轉換為原始值

JavaScript 引擎內部的抽象操作 ToPrimitive() 有着這樣的簽名:

ToPrimitive(input,PreferredType?)

可選參數 PreferredType 可以是 Number 或者 String。 它只代表了一個轉換的偏好,轉換結果不一定必須是這個參數所指的類型(汗),但轉換結果一定是一個原始值。 如果 PreferredType 被標志為 Number,則會進行下面的操作來轉換input

  1. 如果 input 是個原始值,則直接返回它。
  2. 否則,如果 input 是一個對象。則調用 obj.valueOf() 方法。 如果返回值是一個原始值,則返回這個原始值。
  3. 否則,調用 obj.toString() 方法。 如果返回值是一個原始值,則返回這個原始值。
  4. 否則,拋出 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

在計算這個表達式時,內部的操作步驟是這樣的

  1. 將兩個操作數轉換為原始值 (以下是數學表示法的偽代碼,不是可以運行的 JavaScript 代碼):
     prim1 := ToPrimitive(value1)
     prim2 := ToPrimitive(value2)

    PreferredType 被省略,因此 Date 類型的值采用 String,其他類型的值采用 Number。

  2. 如果 prim1 或者 prim2 中的任意一個為字符串,則將另外一個也轉換成字符串,然后返回兩個字符串連接操作后的結果。
  3. 否則,將 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]'


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM