- Published on
JS中的 {} + {} 與 {} + [] 的結果是什麼?
- Authors
- Name
- Eddy Chang
在 JS 中的運算符共同的情況中,(+)符號是很常見的一種,它有以下的使用情況:
- 數字的加法運算,二元運算
- 字串的連接運算,二元運算,最高優先
- 正號,一元運算,可延伸為強制轉換其他類型的運算元為數字類型
當然,如果考慮多個符號一起使用時,(+=)與(++)又是另外的用途。
另一個常見的是花括號(),它有兩個用途也很常見:
- 物件的字面文字定義
- 區塊語句
所以,要能回答這個問題,要先搞清楚重點是什麼?
第一個重點是:
加號(+)運算在 JS 中在使用上的規定是什麼。
第二個重點則是:
物件在 JS 中是怎麼轉換為原始資料類型的值的。
加號運算符(+)
除了上面說明的常見情況外,在標準中轉換的規則還有以下幾個,要注意它的順序:
operand + operand = result
- 使用
ToPrimitive
運算轉換左與右運算元為原始資料類型值(primitive) - 在第 1 步轉換後,如果有運算元出現原始資料類型是"字串"類型值時,則另一運算元作強制轉換為字串,然後作字串的連接運算(concatenation)
- 在其他情況時,所有運算元都會轉換為原始資料類型的"數字"類型值,然後作數學的相加運算(addition)
ToPrimitive 內部運算
因此,加號運算符只能使用於原始資料類型,那麼對於物件類型的值,要如何轉換為原始資料類型?下面說明是如何轉換為原始資料類型的。
在ECMAScript 6th Edition #7.1.1,有一個抽象的ToPrimitive
運算,它會用於物件轉換為原始資料類型,這個運算不只會用在加號運算符,也會用在關係比較或值相等比較的運算中。下面有關於ToPrimitive
的說明語法:
ToPrimitive(input, PreferredType?)
input
代表代入的值,而PreferredType
可以是數字(Number)或字串(String)其中一種,這會代表"優先的"、"首選的"的要進行轉換到哪一種原始類型,轉換的步驟會依這裡的值而有所不同。但如果沒有提供這個值也就是預設情況,則會設定轉換的hint
值為"default"
。這個首選的轉換原始類型的指示(hint
值),是在作內部轉換時由 JS 視情況自動加上的,一般情況就是預設值。
而在 JS 的Object
原型的設計中,都一定會有兩個valueOf
與toString
方法,所以這兩個方法在所有物件裡面都會有,不過它們在轉換有可能會交換被呼叫的順序。
當 PreferredType 為數字(Number)時
當PreferredType
為數字(Number)時,input
為要被轉換的值,以下是轉換這個input
值的步驟:
- 如果
input
是原始資料類型,則直接回傳input
。 - 否則,如果
input
是個物件時,則呼叫物件的valueOf()
方法,如果能得到原始資料類型的值,則回傳這個值。 - 否則,如果
input
是個物件時,呼叫物件的toString()
方法,如果能得到原始資料類型的值,則回傳這個值。 - 否則,拋出 TypeError 錯誤。
當 PreferredType 為字串(String)時
上面的步驟 2 與 3 對調,如同下面所說:
- 如果
input
是原始資料類型,則直接回傳input
。 - 否則,如果
input
是個物件時,呼叫物件的toString()
方法,如果能得到原始資料類型的值,則回傳這個值。 - 否則,如果
input
是個物件時,則呼叫物件的valueOf()
方法,如果能得到原始資料類型的值,則回傳這個值。 - 否則,拋出 TypeError 錯誤。
PreferredType 沒提供時,也就是 hint 為"default"時
與PreferredType
為數字(Number)時的步驟相同。
數字其實是預設的首選類型,也就是說在一般情況下,加號運算中的物件要作轉型時,都是先呼叫
valueOf
再呼叫toString
。
但這有兩個例外,一個是Date
物件,另一是Symbol
物件,它們覆蓋了原來的PreferredType
行為,Date
物件的預設首選類型是字串(String)。
因此你會看到在一些教學文件上會區分為兩大類物件,一類是 Date 物件,另一類叫 非 Date(non-date) 物件。因為這兩大類的物件在進行轉換為原始資料類型時,首選類型恰好相反。
模擬程式碼說明
以簡單的模擬程式碼來說明,加號運算符(+)的執行過程就是像下面這個模擬碼一樣,我想這會很容易理解:
a + b:
pa = ToPrimitive(a)
pb = ToPrimitive(b)
if(pa is string || pb is string)
return concat(ToString(pa), ToString(pb))
else
return add(ToNumber(pa), ToNumber(pb))
步驟簡單來說就是,運算元都用ToPrimitive
先轉換為原始資料類型,然後其一是字串時,使用ToString
強制轉換另一個運算元,然後作字串連接運算。要不然,就是都使用ToNumber
強制轉換為數字作加法運算。
而ToPrimitive
在遇到物件類型時,預設呼叫方式是先呼叫valueOf
再呼叫toString
,一般情況數字類型是首選類型。
上面說的ToString
與ToNumber
這兩個也是 JS 內部的抽象運算。
valueOf 與 toString 方法
valueOf
與ToString
是在 Object 中的兩個必有的方法,位於 Object.prototype 上,它是物件要轉為原始資料類型的兩個姐妹方法。從上面的內容已經可以看到,ToPrimitive
這個抽象的內部運算,會依照設定的首選的類型,決定要先後呼叫valueOf
與toString
方法的順序,當數字為首選類型時,優先使用valueOf
,然後再呼叫toString
。當字串為首選類型時,則是相反的順序。預設呼叫方式則是如數字首選類型一樣,是先呼叫valueOf
再呼叫toString
。
JS 對於 Object 與 Array 的設計
在 JS 中所設計的Object
純物件類型的valueOf
與toString
方法,它們的回傳如下:
valueOf
方法回傳值: 物件本身。toString
方法回傳值: "[object Object]"字串值,不同的內建物件的回傳值是"[object type]"字串,"type"指的是物件本身的類型識別,例如 Math 物件是回傳"[object Math]"字串。但有些內建物件因為覆蓋了這個方法,所以直接呼叫時不是這種值。(注意: 這個回傳字串的前面的"object"開頭英文是小寫,後面開頭英文是大寫)
你有可能會看過,利用 Object 中的 toString 來進行各種不同物件的判斷語法,這在以前 JS 能用的函式庫或方法不多的年代經常看到,不過它需要配合使用函式中的call
方法,才能輸出正確的物件類型值,例如:
> Object.prototype.toString.call([])
"[object Array]"
> Object.prototype.toString.call(new Date)
"[object Date]"
所以,從上面的內容就可以知道,下面的這段程式碼的結果會是呼叫到toString
方法(因為valueOf
方法的回傳並不是原始的資料類型):
> 1 + {}
"1[object Object]"
一元正號(+),具有讓首選類型(也就是 hint)設置為數字(Number)的功能,所以可以強制讓物件轉為數字類型,一般的物件會轉為:
;+{} //相當於 +"[object Object]"
// NaN
當然,物件的這兩個方法都可以被覆蓋,你可以用下面的程式碼來觀察這兩個方法的執行順序,下面這個都是先呼叫valueOf
的情況:
let obj = {
valueOf: function() {
console.log('valueOf')
return {} // object
},
toString: function() {
console.log('toString')
return 'obj' // string
},
}
console.log(1 + obj) //valueOf -> toString -> '1obj'
console.log(+obj) //valueOf -> toString -> NaN
console.log('' + obj) //valueOf -> toString -> 'obj'
先呼叫toString
的情況比較少見,大概只有Date
物件或強制要轉換為字串時才會看到:
let obj = {
valueOf: function() {
console.log('valueOf')
return 1 // number
},
toString: function() {
console.log('toString')
return {} // object
},
}
alert(obj) //toString -> valueOf -> alert("1");
String(obj) //toString -> valueOf -> "1";
而下面這個例子會造成錯誤,因為不論順序是如何都得不到原始資料類型的值,錯誤訊息是"TypeError: Cannot convert object to primitive value",從這個訊息中很明白的告訴你,它這裡面會需要轉換物件到原始資料類型:
let obj = {
valueOf: function() {
console.log('valueOf')
return {} // object
},
toString: function() {
console.log('toString')
return {} // string
},
}
console.log(obj + obj) //valueOf -> toString -> error!
Array(陣列)很常用到,雖然它是個物件類型,但它與 Object 的設計不同,它的toString
有覆蓋,說明一下陣列的valueOf
與toString
的兩個方法的回傳值:
valueOf
方法回傳值: 物件本身。(與 Object 一樣)toString
方法回傳值: 相當於用陣列值呼叫join(',')
所回傳的字串。也就是[1,2,3].toString()
會是"1,2,3"
,這點要特別注意。
Function 物件很少會用到,它的toString
也有被覆蓋,所以並不是 Object 中的那個toString
,Function 物件的valueOf
與toString
的兩個方法的回傳值:
valueOf
方法回傳值: 物件本身。(與 Object 一樣)toString
方法回傳值: 函式中包含的程式碼轉為字串值
Number、String、Boolean 三個包裝物件
包裝物件是 JS 為原始資料類型數字、字串、布林專門設計的物件,所有的這三種原始資料類型所使用到的屬性與方法,都是在這上面所提供。
包裝物件的valueOf
與toString
的兩個方法在原型上有經過覆蓋,所以它們的回傳值與一般的 Object 的設計不同:
valueOf
方法回傳值: 對應的原始資料類型值toString
方法回傳值: 對應的原始資料類型值,轉換為字串類型時的字串值
toString
方法會比較特別,這三個包裝物件裡的toString
的細部說明如下:
- Number 包裝物件的
toString
方法: 可以有一個傳入參數,可以決定轉換為字串時的進位(2、8、16) - String 包裝物件的
toString
方法: 與 String 包裝物件中的valueOf
相同回傳結果 - Boolean 包裝物件的
toString
方法: 回傳"true"或"false"字串
另外,常被搞混的是直接使用Number()
、String()
與Boolean()
三個強制轉換函式的用法,這與包裝物件的用法不同,包裝物件是必須使用new
關鍵字進行物件實體化的,例如new Number(123)
,而Number('123')
則是強制轉換其他類型為數字類型的函式。
Number()
、String()
與Boolean()
三個強制轉換函式,所對應的就是在 ECMAScript 標準中的ToNumber
、ToString
、ToBoolean
三個內部運算轉換的對照表。而當它們要轉換物件類型前,會先用上面說的ToPrimitive
先轉換物件為原始資料類型,再進行轉換到所要的類型值。
不管如何,包裝物件很少會被使用到,一般我們只會直接使用原始資料類型的值。而強制轉換函式因為也有取代的語法,它們會被用到的機會也不多。
應用實例
字串 + 其他原始類型
字串在加號運算有最高的優先運算,與字串相加必定是字串連接運算(concatenation)。所有的其他原始資料類型轉為字串,可以參考 ECMAScript 標準中的ToString對照表,以下為一些簡單的範例:
> '1' + 123
"1123"
> '1' + false
"1false"
> '1' + null
"1null"
> '1' + undefined
"1undefined"
數字 + 其他的非字串的原始資料類型
數字與其他類型作相加時,除了字串會優先使用字串連接運算(concatenation)的,其他都要依照數字為優先,所以除了字串之外的其他原始資料類型,都要轉換為數字來進行數學的相加運算。如果明白這項規則,就會很容易的得出加法運算的結果。
所有轉為數字類型可以參考 ECMAScript 標準中的ToNumber對照表,以下為一些簡單的範例:
> 1 + true //true轉為1, false轉為0
2
> 1 + null //null轉為0
1
> 1 + undefined //null轉為NaN
NaN
數字/字串以外的原始資料類型作加法運算
所以,當數字與字串以外的其他原始資料類型直接使用加號運算時,就是轉為數字再運算,這與字串無關。
> true + true
2
> true + null
1
> undefined + null
NaN
空陣列 + 空陣列
> [] + []
""
兩個陣列相加,依然按照valueOf -> toString
的順序,但因為valueOf
是陣列本身,所以會以toString
的回傳值才是原始資料類型,也就是空字串,所以這個運算相當於兩個空字串在相加,依照加法運算規則第 2 步驟,是字串連接運算(concatenation),兩個空字串連接最後得出一個空字串。
空物件 + 空物件
> {} + {}
"[object Object][object Object]"
兩個空物件相加,依然按照valueOf -> toString
的順序,但因為valueOf
是物件本身,所以會以toString
的回傳值才是原始資料類型,也就是"[object Object]"字串,所以這個運算相當於兩個"[object Object]"字串在相加,依照加法運算規則第 2 步驟,是字串連接運算(concatenation),最後得出一個"[object Object][object object]"字串。
但是這個結果有例外,上面的結果只是在 Chrome 瀏覽器上的結果(v55),怎麼說呢?
有些瀏覽器例如 Firefox、Edge 瀏覽器會把{} + {}
直譯為相當於+{}
語句,因為它們會認為以花括號開頭({
)的,是一個區塊語句的開頭,而不是一個物件字面量的語句,所以會認為略過第一個{}
,把它認為是個+{}
的語句,也就是相當於強制求出數字值的Number({})
運算,相當於Number("[object Object]")
運算,最後得出的是NaN
。
特別注意:
{} + {}
在不同的瀏覽器有不同結果
如果在第一個(前面)的空物件加上圓括號(()
),這樣 JS 就會認為前面是個物件,就可以得出同樣的結果:
> ({}) + {}
"[object Object][object Object]"
或是分開來先定義物件的變數值,也可以得出同樣的結果,像下面這樣:
> let foo = {}, bar = {};
> foo + bar;
註: 上面說的行為這與加號運算的第一個(前面)的物件字面值是不是個空物件無關,就算是裡面有值的物件字面,例如
{a:1, b:2}
,也是同樣的結果。
空物件 + 空陣列
上面同樣的把{}
當作區塊語句的情況又會發生,不過這次所有的瀏覽器都會有一致結果,如果{}
(空物件)在前面,而[]
(空陣列)在後面時,前面那個會被認為是區塊而不是物件。
所以{} + []
相當於+[]
語句,也就是相當於強制求出數字值的Number([])
運算,相當於Number("")
運算,最後得出的是0
數字。
> {} + []
0
> [] + {}
"[object Object]"
特別注意: 所以如果第一個(前面)是
{}
時,後面加上其他的像陣列、數字或字串,這時候加號運算會直接變為一元正號運算,也就是強制轉為數字的運算。這是個陷阱要小心。
Date 物件
Date 物件的valueOf
與toString
的兩個方法的回傳值:
valueOf
方法回傳值:toString
方法回傳值:
Date 物件上面有提及是首選類型為"字串"的一種例外的物件,這與其他的物件的行為不同(一般物件會先呼叫valueOf
再呼叫toString
),在進行加號運算時時,它會優先使用toString
來進行轉換,最後必定是字串連接運算(concatenation),例如以下的結果:
> 1 + (new Date())
> "1Sun Nov 27 2016 01:09:03 GMT+0800 (CST)"
要得出 Date 物件中的valueOf
回傳值,需要使用一元加號(+),來強制轉換它為數字類型,例如以下的程式碼:
> +new Date()
1480180751492
Symbols 類型
ES6 中新加入的 Symbols 資料類型,它不算是值也不是物件,所以完全不能直接用於加法運算,它並沒有內部自動轉型的設計,使用時會報錯。
結論
{} + {}
的結果是會因瀏覽器而有不同結果,Chrome 中是[object Object][object Object]
字串連接,但其它的瀏覽器則是認為相當於+{}
運算,得出NaN
數字類型。
{} + []
的結果是相當於+[]
,結果是0
數字類型。