一路上感謝各位讀者們的支持和回饋。
本 30 天系列文目前已經將篇幅重新整理、編纂成冊。
《JavaScript 概念三明治》在天瓏書局上架囉!
喜歡這個系列,想閱讀更詳細原理說明的讀者可以參考:
https://www.tenlong.com.tw/products/9789864347575
當 JS 想要對兩個不同型別的數值做運算時, 這兩個數值必須都是同一種類型。這時有兩種方式來達成轉型,一種是使用 JS 內建的方法,像是 toString() 方法來達成;另外一種方法是,如果開發者沒有主動使用 JS 的轉型方法,那麼 JS 就會「貼心」地自動幫你轉型。上述提到的兩種方法,前者稱為「明確的」轉型、後者則是「隱含的」轉型。
當開發者明確的告訴 JS 要轉型的意圖時,例如使用 Number('2'),這個動作就稱為「明確的」強制轉型。而也因為 JS 是弱型別的語言,所以數值型別本來就能夠自由地轉換,因此有了「隱含」的轉型的現象出現。雖然「隱含的」轉型聽起來,可以交給 JS 處理就好,什麼事都不用做。不過很可惜,這件事情在後人看來,似乎反倒造成許多不必要的困擾與誤解,因此通常不建議使用。
強制轉型只有三種類型:
另外,提醒一下純值( Primitive )與 物件型別的強制轉型的運作邏輯不太一樣,(細節後面會提到),不過不管是哪一種類別都能夠執行上面的三種轉型方式,接下來會分別對上述幾種轉型方式做說明。
明確強制轉型:要將字串明確的轉型,可以使用 String ('2') 。
隱含強制轉型: 當使用到 '+' 運算子的時候就會觸發隱含的轉型。
String(123) // 明確地
123 + '' // 隱含地
而所有的其他類型純值的轉型,你應該都能夠正確預測出來 :
String(111) // ---> '111'
String(-12.3) // ---> '-12.3'
String(null) // ---> 'null'
String(undefined) // ---> 'undefined'
String(true) // ---> 'true'
String(false) // ---> 'false'
Symbol 的部分目前有點怪異,只能夠通過明確的轉型來執行,如果你企圖使用 '+' 運算子想要達成隱含的轉型,就會失敗:
String(Symbol('my symbol')) // 'Symbol(my symbol)'
'' + Symbol('my symbol') // Cannot convert a Symbol value to a string
明確強制轉型:布林數值的強制轉型跟字串很像,也是使用 Boolean() 方法來轉型。
隱含強制轉型:則是透過邏輯運算子以及像是 if 判斷式內的條件區塊來觸發:
Boolean(2) // 明確轉型
if ('yeah') { ... } // if 或 while 等陳述式條件區塊
2 || 'hello' // 邏輯運算子觸發
!!2 // 邏輯運算子觸發
Boolean 值不管再怎麼轉型,最後都只會有兩種結果,其中有一些值一定會被轉型為 false ,JS 裡面用 falsy value 來描述那些必定會被轉型為 false 的數值,這部分在下一章節會有額外說明:
Boolean('') // false
Boolean(0) // false
Boolean(-0) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false
那麼我們要怎麼判斷哪些值會被轉型為 true 呢? 很簡單:除了 falsy value 以外的值就會被轉為 true ,所以只要搞清楚上述幾種情況即可,以下舉例被轉型為 ture 的情況( Trusy Value ):
Boolean({}) // 空物件也是 true !
Boolean(function() {}) // function 也是物件喔!
Boolean([]) // 空陣列也是物件喔!
Boolean(Symbol()) // true
!!Symbol() // true
這邊提醒一下,在比較兩個數值時,盡量使用三個等號來判斷,而不是兩個。當你使用三個等號來判斷時,除非兩邊的型別與值都完全相同,否則結果一定會是 false ,這時候因為不會有需要轉型的情況發生,減少了許多誤判與產生 bug 的機會。
明確強制轉型:
數字的明確轉型也跟布林以及字串的轉型差不多,使用 Number('12') 就能將其他類型轉為數字。
隱含強制轉型:
數字的隱含轉型也是透過運算子觸發,不過因為常常牽扯到運算,因此會有比較多種情況需要說明:
比較運算子 ( >, <, <=,>= )
移位運算子 ( bitwise operators : | & ^ ~ )
算數運算子 (- + * / % ),這邊要記得,當使用 '+' 來計算結果的時候,只要其中一個數值是字串,那麼就不會觸發數字類型的隱含轉型( 會轉為字串,這是 JS 的規則 )
單純使用 + 來做數字轉型( ex. 在任一其他類型最前面加上 '+' )
寬鬆的數值比較( == 及 ==)
Number('123') // 明確的
+'123' // 隱含的
4 > '5' // 隱含的
1+null // 隱含的
123 != '456' // 寬鬆的數值比較
來看一下各種純值類型是怎麼被轉成數值的:
Number(123) // 123
Number(" 12 ") // 12
Number("-12.34") // -12.34
Number(true) // 1
Number(null) // 0
Number(false) // 0
Number("\n") // 0
Number(" 12s ") // NaN
Number(undefined) // NaN
可以看到上面當我們想將含有空白的字串 ' 12 ‘ 做強制轉型,卻沒有失敗。這是因為 JS 在做轉型之前,會先幫我們把字串的頭尾空白都消除掉( trim ),所以不會報錯。但是如果這個處理過的字串還是無法被轉為數字,那麼 JS 就會因為轉型失敗而得到 NaN 的結果。
比較特別的部分還有,null 會被轉型為 0 而 undefined 會被轉成 NaN ( Not a Number ) ,這是 JS 的規則,我覺得知道就好沒必要特別背起來。Symbol 類型,則不管透過哪一種方式,都無法被轉為數字,所以都會報錯。
當你把物件與運算子一起使用時,JS 必須先把物件都轉為純值,才有辦法做運算,得出最後結果,這時就需要探討物件轉型的規則了。
最簡單易懂的部分是物件轉為布林值的邏輯,對物件轉型為布林來說,任何非純值類型的數值,都會被轉為 true。
在將物件轉型為數字與字串的時候,JavaScript 根據 ECMAScript 規範,使用一個叫做 ToPrimitive 的演算流程,這個流程分別使用 toString 與 valueOf 兩個方法搭配整個物件當作輸入值來判斷結果,而這兩個方法在所有是物件類型的數值上都可以呼叫,也可以透過複寫來自定。
通常物件都會有預設的 toString 方法。如果宣告一個物件變數,並直接呼叫其上面的 toString() 方法,就會得到 [object Object] ,的字串結果,這是物件被轉為字串型別時的預設值。
let a = {}
a.toString() // [object Object]
詳細說明可以參考 MDN 官方文件
簡單介紹一下 ToPrimitive 運作流程
如果輸入值已經是純值,那麼直接回傳這個值。
試試呼叫這個數入數值 (這個物件/陣列)上的 toString 方法,如果結果是純值,則回傳。
試試呼叫這個數入數值 (這個物件/陣列)上的 valueOf 方法,如果結果是純值,則回傳。
如果執行前兩個方法都沒辦法得到純值的型別,就會報錯 (TypeError)。
上面有提到執行 toString() 與 valueOf() 的部分,若 JS 判斷需要轉為數字,則優先執行 valueOf 方法,若失敗才轉為執行 toString ,反之若需要轉為字串則相反,依照上述原來的順序。
//會使用到陣列內建的 toString 方法,將所有陣列值都轉為字串
[1]+[2,3] // "12,3"
好,經過這麼多複雜跟瑣碎的說明,恭喜你看到這裡,現在你知道:
雖然強制轉型很複雜而且繁瑣,而且我覺得只要只用全等於就能夠過減少一半的錯誤發生機率,但只要寫程式夠久,總會有需要看髒 Code 的時候。知道有轉型這件事情以及為什麼會發生,對之後要 debug 一定會有幫助。謝謝你看到這裡,那麼,在下一章節,我會針對 Trusy ,跟 Falsy 做一個比較詳細的整理。