iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 10
1
Modern Web

JavaScript 原力覺醒 - 成為絕地武士之路系列 第 10

JS 原力覺醒 Day010 - 自動轉型 (Coercion)

當 JS 想要對兩個不同型別的數值做運算時, 這兩個數值必須都是同一種類型。這時有兩種方式來達成轉型,一種是使用 JS 內建的方法,像是 toString() 方法來達成;另外一種方法是,如果開發者沒有主動使用 JS 的轉型方法,那麼 JS 就會「貼心」地自動幫你轉型。上述提到的兩種方法,前者稱為「明確的」轉型、後者則是「隱含的」轉型。

Outline

  • 強制轉型 : 明確?不明確?
  • 轉為字串的轉型
  • 轉為布林的轉型
  • 轉為數字的轉型
  • 物件的強制轉型

強制轉型 : 明確?不明確?

當開發者明確的告訴 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 就會回傳錯誤而中止。

比較特別的部分還有,null 會被轉型為 0 而 undefined 會被轉成 NaN ( Not a Number ) ,這是 JS 的規則,我覺得知道就好沒必要特別背起來。Symbol 類型,則不管透過哪一種方式,都無法被轉為數字,所以都會報錯。

物件的強制轉型

當你把物件與運算子一起使用時,JS 必須先把物件都轉為純值,才有辦法做運算,得出最後結果,這時就需要探討物件轉型的規則了。

物件轉布林值

最簡單易懂的部分是物件轉為布林值的邏輯,對物件轉型為布林來說,任何非純值類型的數值,都會被轉為 true。

物件轉字串與數字

對於物件被轉為數字與字串的部分,JS 使用一個叫做 ToPrimitive 的演算流程,這個流程分別使用 toString() 與 valueOf() 搭配整個物件當作此方法的輸入值來判斷結果。由於這兩個都存在於 Object 原型鏈上,所以所有是物件類型的值 ( Object 的後代 ) 都可以呼叫,也可以透過複寫來自定。

通常物件都會有預設的 toString 方法 ,但沒有預設的 valueOf 方法。例如你如果宣告一個物件變數,然後直接呼叫其上面的 toString() 方法,就會得到 「 [object Object] 」,的結果,這是因為,物件預設是以 typeof 的結果來當作回傳值。

let a = {} 
a.toString() // [object Object]

詳細說明可以參考 MDN 官方文件

簡單介紹一下 ToPrimitive 運作流程

  1. 如果輸入值已經是純值,那麼直接回傳這個值。

  2. 試試呼叫這個數入數值 (這個物件/陣列)上的 toString 方法,如果結果是純值,則回傳。

  3. 試試呼叫這個數入數值 (這個物件/陣列)上的 valueOf 方法,如果結果是純值,則回傳。

  4. 如果執行前兩個方法都沒辦法得到純值的型別,就會報錯 (TypeError)。

上面有提到執行 toString() 與 valueOf() 的部分,若 JS 判斷需要轉為數字,則優先執行 valueOf 方法,若失敗才轉為執行 toString ,反之若需要轉為字串則相反,依照上述原來的順序。

//會使用到陣列內建的 toString 方法,將所有陣列值都轉為字串
[1]+[2,3] // "12,3"

結論

好,經過這麼多複雜跟瑣碎的說明,恭喜你看到這裡,現在你知道:

  1. 轉型有幾種類別:分別是轉為字串、數值跟布林
  2. 轉型可以透過兩種方式:明確的 ( explicit ) 以及隱含的( Implicit )
  3. 必要時物件也會被強制轉型,且是透過 ToPrimitive 演算流程來判斷結果
  4. 判斷兩值是否相等時,盡量使用(全等於)三個等號來判斷
  5. 判斷布林值的轉型時,只要不是 Falsy 的值,就會被轉型為 true

雖然強制轉型很複雜而且繁瑣,而且我覺得只要只用全等於就能夠過減少一半的錯誤發生機率,但只要寫程式夠久,總會有需要看髒 Code 的時候。知道有轉型這件事情以及為什麼會發生,對之後要 debug 一定會有幫助。謝謝你看到這裡,那麼,在下一章節,我會針對 Trusy ,跟 Falsy 做一個比較詳細的整理。


上一篇
JS 原力覺醒 Day9 - 原始型別與物件型別
下一篇
JS 原力覺醒 Day11 - Falsy / Truthy
系列文
JavaScript 原力覺醒 - 成為絕地武士之路30

尚未有邦友留言

立即登入留言