[JS] 強制轉型及轉換技巧


Posted by Genos on 2021-11-18

JavaScript 不論遇到什麼奇形怪狀的資料運算,都會竭盡所能的轉換,好讓程式執行下去;反觀其他程式語言只要型別沒轉好,絕對報錯報到飽。

這麼貼心的型別轉換也是兩面刃,當特殊情況發生報錯的時候讓絕對 debug 半天滿頭問號,馬上來認識 JavaScript 的轉型吧!


強制轉型(coercion)

強制轉型分為兩種

  • 顯性轉型(explicit coercion)
  • 隱性轉型(implicit coercion)

💎 顯性轉型(explicit coercion)

顯性轉型是指在程式碼直接撰寫型別轉換。

🔸 轉數字

  • parseInt()

    • 轉整數。
    • 小數點以後無條件捨去。
    • 轉換方式:由左至右,遇到無法轉成數字的時候停止,回傳前面的數字。
      console.log(parseInt('1.23')); // 1
      console.log(parseInt('1.23a')); // 1
      console.log(parseInt('a1.23')); // NaN
      
  • parseFloat()

    • 轉浮點數(包含小數點以後)。
    • 轉換方式與 parseInt() 相同。
      console.log(parseFloat('1.23')); // 1.23
      
  • Number()

    • 可以轉整數和浮點數。
    • 數字範圍在正負 2的53次方 -1 間。
    • 轉換方式:整筆字串都可以轉成數字才轉換,否則得到 NaN。
      console.log(Number('-1.23')); // -1.23
      console.log(Number('-1.23a')); // NaN
      
  • BigInt()

    • 轉整數
    • 範圍可以超過2的53次方(表示方式會在數字後面加上 n
    • 轉換方式:非整數時直接報錯,不會得到 NaN。
    • 需注意瀏覽器支援度。
      console.log(BigInt("9007199254740991")); // 9007199254740991n
      console.log(typeof 1n); // bigint
      console.log(BigInt("900a")); // Cannot convert 900a to a BigInt
      
  • 算數運算子:

    • 前方加上+:容易和 ++ 混淆。
    • 前方加上-:內容若是負數,會被轉成正數。
    • 後方加上- 0:較穩定寫法。
      console.log(+'-1'); // -1
      console.log(++'-1'); // Invalid left-hand side expression in prefix operation
      console.log(-'-1'); // 1
      console.log('-1' - 0); // -1
      

🔸 轉字串

  • toString():無法轉換 null 和 undefined、提供數字進位制轉換。
    console.log((3).toString()); // 3
    console.log(undefined.toString()); // Cannot read properties of undefined
    console.log((3).toString(2)); // 11
    
  • String():可以轉換 null 和 undefined、無法提供數字進位制轉換。
    console.log(String(3)); // 3
    console.log(String(undefined)); // undefined
    
  • 算數運算子:加上空字串 + ''
    console.log(typeof (3 + '')); // string
    

🔸 轉布林值

  • 布林值只有兩種結果: true 或 false,轉換後會對應 true 的值稱為 truthy,對應 false 的則是 falsy,屬於 falsy 的有以下幾種,其他的都是 truthy:
    • false
    • 0
    • -0
    • 0n (BigInt)
    • “” (空字串,包含 ``, ‘’)
    • null
    • undefined
    • NaN
    • document.all (正常情況下不會用到)
  • 轉換方式:
    • Boolean()
      console.log(Boolean([])); // true
      
    • 邏輯運算子:前方加上雙驚嘆號 !!,單驚嘆號 ! 會是相反結果。
      console.log(!true); // false
      console.log(!!{}); // true
      

💎 隱性轉型(implicit coercion)

隱性轉型出現在使用運算子時,這在其他程式語言通常是不允許的(需要相同型別才可以運算),JavaScript 則會自動轉型讓程式繼續執行,也因為規則繁雜,容易疏忽出錯。

🔸 認識核心的 toPrimitive

JavaScript 在執行運算時會使用 toPrimitive 將運算元轉換成 基本型別(Primitives)才能繼續運算,toPrimitive 帶有一個 hint(提示),用來決定要轉成什麼型別,下面的例子會逐步說明。

🔸 + 算數運算子

  • + 運算中只要有一個屬於字串型別,就會轉為 string,最高優先;一般情況都是轉成 number。
    // 有字串
    console.log('' + 1 + null + true); // 1nulltrue
    // 無字串
    console.log(1 + null + true); // 2
    

🔸 其他算數運算子

  • -*/% 運算都會轉成 number。
    console.log('6' - undefined); // NaN
    console.log(9 * [9]); // 81
    console.log('3' / {valueOf: function(){ return 3}}); // 1
    console.log(6 % true); // 0
    

🔸 object 的轉型機制

  • toPrimitive執行物件類型的型別轉換時會呼叫 valueOftoString 方法來取得原始型別的回傳值(預設先轉數字,不能轉數字再轉字串),以下範例先宣告 4 個物件來,並覆蓋預設的回傳值:

    // 覆蓋 valueOf 回傳值、toString 回傳值
    let a = {
      valueOf: function() {
          return 1;
      },
      toString: function() {
          return 2;
      }
    };
    // 只覆蓋 toString 的回傳值
    let b = {
      toString: function() {
          return 3;
      }
    };
    // 覆蓋 valueOf 回傳值、toString 回傳值,但是回傳物件
    let c = {
      valueOf: function() {
          return [];
      },
      toString: function() {
          return {};
      }
    };
    // 空物件,使用預設值
    let d = {};
    

    先轉數字,不能轉數字再轉字串

    // 先取 valueOf 成功,後方運算元為字串(最高優先),故轉字串運算
    console.log(a + ''); // "1"
    // 先取 valueOf 失敗,改取 toString,後方運算元為數字,故轉數字運算
    console.log(b + 0); // 3
    // 取不到原始型別,就會報錯
    console.log(c + 0); // Cannot convert object to primitive value
    

    指定為字串時,呼叫 toString() 方法,

    // 指定取 toString
    String(a); // "2"
    // 取不到原始型別,一樣報錯
    String(c); // Cannot convert object to primitive value
    // 返回 toString() 預設的回傳值
    String(d); // "[object Object]"
    

    物件類型的 valueOf 是物件本身,物件本身並不是 原始型別,所以上面 String(d) 的範例可以看到,在沒有修改回傳值的情況下,會取得 toString() 的回傳值。

  • 陣列也是物件,所以 valueOf() 同樣是自己本身(非原始型別),會回傳 toString() 的字串。

    // 空陣列轉字串 "",空物件轉字串 "[object Object]"
    console.log([] + {}); // "[object Object]"
    // 陣列轉字串 "1,2,3",後方數字配合轉字串
    console.log([1,2,3] + 2 + 1); // "1,2,321"
    
  • {} + x 這種類型牽涉到兩個觀念:
    1. {} 在運算元前方時會被視為區塊語句,而不是物件,所以實際執行的只有後面的 + x
    2. + x是前一段落提到的顯性轉型轉數字的方法,所以後方的運算元轉為 number 型別。
      // 強制轉型
      console.log(+{}); // NaN
      // 表面上是物件加陣列,實際上是後方的陣列強制轉型為數字
      console.log({} + []); // 0 
      // 上方程式實際執行如下面兩行:
      {}
      +[];
      // 宣告一個物件變數 x 來存放空物件,才能被當成物件來計算
      let x = {};
      console.log(x + []); // "[object Object]"
      
  • {} + {}
    這又是一個特別狀況所以被獨立出來,因為不同的瀏覽器會有不同的執行結果:
    1. NaN - 第一個 {} 被視為區塊
    2. "[object Object][object Object]" - 第一個 {} 被視為物件

🔸 邏輯運算子

邏輯運算中的運算元都會被轉為 boolean,差別在於其運算後回傳的結果。

  • || (or) 會回傳第一個結果為 true 的運算元,若無,則是最後一個。
    console.log(0 || false || null || undefined); // undefined
    console.log(0 || false || null || {} || undefined); // {}
    
  • && (and) 若運算結果為 true,會回傳最後一個運算元,若運算結果為 false,回傳第一個結果為 false 的運算元。
    console.log(true && {} && -2 && ['a']); // ['a']
    console.log(1 && 0 && true & false); // 0
    
    ## 💎 結語
    關於轉型的細節實在太多,族繁不及備載,一些極端的範例在實際撰寫時並不會用到,不求成為行走的 MDN,但求踩坑的時候能順利 debug 就好!

參考文章
MDN-BigInt
sunnyhuang-何謂強制轉型(coercion)以及如何作到轉換型別
MDN-Number.prototype.toString()
淺談JS中String()與.toString()的區別 - 程式前沿
MDN-Symbol.toPrimitive
Eddy 思考與學習-JS中的 {} + {} 與 {} + [] 的結果是什麼?


#javascript







Related Posts

Scrapy 和 Redis 分散式crawlers (蘋果日報為例)

Scrapy 和 Redis 分散式crawlers (蘋果日報為例)

Decoding A/B Testing: The Magic of the Paired T-Test

Decoding A/B Testing: The Magic of the Paired T-Test

原型與繼承(1) - 建構式函式 & 閉包

原型與繼承(1) - 建構式函式 & 閉包


Comments