iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 13
0

相信大家在初學 JavaScript 時,一定有人跟你說過:「比較時記得要用三等號(===),不要用雙等號(==)」,但你知道為什麼嗎?稍微有經驗的人可能馬上想到了,因為使用雙等號比較時,JavaScript 會做 自動轉型,那麼自動轉型是怎麼轉的呢?背後的機制又是什麼?今天我們就來揭開弱型別的 JavaScript 背後,自動轉型的神秘面紗。

自動轉型的「型」,指的當然是「型別」;因此在真正開始討論之前,你必須要先充分認識 JavaScript 中的基礎型別。我們也在 昨天的文章 中有深入討論這件事情,如果你還沒看過,建議你先去看看,對型別會比較有概念喔!

相等性

在 JavaScript 中有數種 比較運算子,比較特別的是,這邊有兩種相等運算子:一般相等嚴格相等,也就是雙等號及三等號。

嚴格相等

先從較嚴謹的嚴格相等開始看起吧。既然稱為嚴格相等,被比較的雙方除了值相等之外,兩者的型別也必須要相等,比較的結果才會是 true

'foo' === 'foo' // true
123 === 123 // true
123 === '123' // false

另外也補充,昨天有提到 number 型別中的 0-0,在嚴格比較時會被認為是相等的。

一般相等

也有人稱為 寬鬆相等,與嚴格相等相比,會先將比較的雙方自動轉型成同型別的數值,隨後再開始比較,只要「值」一樣便會回傳 true 的結果:

'foo' == 'foo' // true
123 == 123 // true
123 == '123' // true

false == '' // true
false == '0' // true
'' == '0' // false

這裡提到的的「自動轉型」,也就是 JavaScript 的判斷句讓許多開發者感到無比困惑,並建議大家避免使用一般相等的原因了。在本文的後段我們會再仔細端詳它;而這邊只要先知道它和嚴格相等的差異即可。

物件的相等

前述的比較範例中都沒有使用物件,是因為 JavaScript 對物件做相等性比較時,由於物件類型的變數儲存的其實是「記憶體位置」,彼此在比較相等性時,需要指向同一個物件才會得到 true 的比較結果:

let a = {}
let b = {}

a === b // false

let c = {}
let d = c

c === d // true

如果是想要判斷兩物件的屬性是否全部相等,開發者們就必須自行另外撰寫判斷程式來處理。

例外的例外

再補充個例外的例外;沒錯,又是 NaN,它是唯一一個不等於自己的特別數值,無論是嚴格相等或一般相等:

NaN == NaN // false
NaN === NaN // false

一般相等的自動轉型

認識型別及相等性後,我們可以依照 ES6 的規範,仔細瞧瞧在做一般相等的比較時,到底會發生什麼事情。

首先可以觀察到第一條規則,如果等號兩邊的型別相同,則回傳嚴格相等的比較結果,也就不會發生自動轉型了。接下來就是基本型別的轉換,最後是物件轉換,如果不符合任何一條規則,就回傳 false

那麼接下來就讓我們依序確認個別的行為吧。

基本型別轉型

基本型別的轉型比較單純,首先是 nullundefined。在一般相等判斷句中會把 nullundefined 視為相等:

null == undefined // true

symbol 的話,由於是唯一值,不會被轉型成別的 type。

剩下的 numberstringboolean,自動轉型會嘗試把比較的雙方都透過內部的 ToNumber 函式轉成 number 再做比較,例如剛剛奇怪的例子:

false == '' // 轉型後為 0 == 0,true
false == '0' // 轉型後為 0 == 0,true
'' == '0' // 同型別,false

知道會先被轉成數字後,是不是就沒那麼難懂了呢?

物件轉型

接著來看感覺最複雜的物件轉型吧。當物件與基本型別在做相等性判斷時,JavaScript 引擎會讓物件經過內部函式 ToPrimitive,嘗試將物件透過 valueOftoString 等方法轉成基本型別。

值得注意的是,ToPrimitive 函式除了接收物件參數外,還接收一個額外的「PreferredType」參數,而在進行物件轉型時,JavaScript 會依照前後文的語法,自動設定這個參數。

嘗試轉型的方法是什麼呢?開發者可以透過自訂物件中的 Symbol.toPrimitive 屬性,自訂這個轉型的方法。若沒有設定,則會依據 PreferredType 的設定,決定 valueOftoString 的使用順序,依序嘗試轉型,若三個方法都嘗試後仍得不到基本型別,則拋出型別錯誤。

字太多了,直接看範例吧。

let obj = {
  name: 'Gary',
  number: 96.3,
  [Symbol.toPrimitive]: function(hint) {
    // hint 為 'string', 'number', 'default' 三者之一
    return hint === 'string' ? this.name : this.number
  }
}
console.log(+obj) // 96.3
console.log(`${obj}`) // Gary

這邊設定了 Symbol.toPrimitive 屬性,藉由判斷參數 hint,修改了物件轉型的結果。

再看一個範例:

let tmp = {
  toString: () => 'foo',
  valueOf: () => 123
}

// default,優先使用 valueOf()
tmp == 123 // true
tmp == 'foo' // false

// number,優先使用 valueOf()
+tmp == 123 // true
+tmp == 'foo' // false

// string,優先使用 toString()
`${tmp}` == 123 // false
`${tmp}` == 'foo' // true

藉由變數前後的程式碼,設定內部的 PreferredType 屬性,決定 valueOftoString 兩者的使用順序,進而改變了相等性判斷的結果。

牛刀小試

最後來看一個玄妙的題目吧:

// 請問下列這四行的執行結果會得到什麼?
[] + []
[] + {}
{} + []
{} + {}

經過前面的說明,相信讀者應該大概能理解該如何下手了;第一步先確認 []{}valueOftoString 分別是什麼:

[].valueOf() // [],回傳自己
[].toString() // ""
{}.valueOf() // {},回傳自己
{}.toString() // "[object Object]"

接著就一個一個來看吧,首先是 [] + [],由於沒有指定 PreferredType,預設會呼叫 valueOf 方法轉型,但回傳的是本身,仍是物件;接著嘗試呼叫 toString 方法進行轉型,得到的是 "" 空字串,兩個空字串相加後仍是空字串。

第二個是 [] + {},同樣的先嘗試呼叫兩者的 valueOf,都仍然得到物件;接著呼叫 toString,得到 "" + [object Object],最後答案是 [object Object]

第三個 {} + [] 是這邊的陷阱題,由於 {} 會被優先解析成空區塊,沒有任何作用,剩下的 + [] 則會被解析成將 [] 強制轉型成數字;首先呼叫 []valueOf ,仍為物件;接著呼叫 toString,得到基本型別 ""。這時式子變成 + "",因此得到了 0

第四個就非常單純了,把兩個物件 toString 後加起來,得到 [object Object][object Object]

最後的答案如下:

// 請問下列這四行的執行結果會得到什麼?
[] + [] // ""
[] + {} // "[object Object]"
{} + [] // 0
{} + {} // "[object Object][object Object]"

結語

今天我們從相等性比較出發,一窺 JavaScript 中型別轉換的底層機制。雖然本文切入點是一般相等運算子觸發的自動轉型,但由於底層的處理方式都是同樣的內部函式,仍可以將本文的觀念套用到其他的自動轉型運算上;若讀者有需要,也可以直接參考 ES6 文件的相關章節,其實真的蠻清楚好懂的!

關於標題,其實我個人覺得不是不能用,只是要每個工程師都完全熟悉一般相等的特性;不過每個人對熟悉的標準也不一樣,難保不會出現預期外的結果,加上在正式的工作開發中應該要盡可能的避免可能發生的錯誤,因此普遍來說團隊都會要求使用嚴格相等來取代一般相等。

那麼關於型別與自動轉型的部分就到這邊結束啦,接下來的旅程會前進到哪,還請大家拭目以待囉~

參考資料

筆者

Gary

半路出家網站工程師;半生熟的前端加上一點點的後端。
喜歡音樂,喜歡學習、分享,也喜歡當個遊戲宅。

相信一切安排都是最好的路。


上一篇
12. [JS] 為什麼 typeof new Array() === 'object'?
下一篇
14. [JS] 深拷貝是什麼?如何實現?
系列文
前端三十 - 成為更好的前端工程師31

尚未有邦友留言

立即登入留言