[20] 強制轉型 - 轉換值、ToString、JSON


keywords:type conversion,type coercion,ToString,JSON
強制轉型一直被視為是邪惡的、令人困惑的及根本的壞東西,不過我們似乎都沒有完整地理解它,它其實有好處與壞處

轉換值

  • 根據 MDN 文檔 型別轉換分兩種
    1.Type Conversion ( type casting ) 發生於靜態型別語言的編譯時期,可以是隱含的或明確的
    2.Type Coercion 發生於動態型別語言在執行時期所進行的轉換,只會是隱含的

  • 如果一個值的型別明確地轉換成另一個型別,通常叫做型別轉換 ( type casting )
    例如:從字串轉換為數字 Number('99'); // 99

  • 但如果一個值的使用規則而被迫得隱含地進行,稱為強制轉型 ( coercion )
    例如:從字串轉換為數字 +'99'; // 99

  • JavaScript coercion 的結果永遠都會是純量的基本型別值,像是 string、 number、boolean,不會有任何強制轉型的結果是像 object、function 這樣複雜的值

  • 然而多數人都將所有的轉換稱為 coercion ,所以作者把它們分為兩種:
    1.如果能從程式碼明顯看出某個型別轉換動作是刻意進行的,就是『 明確的強制轉型( explicit coercion ) 』
    2.如果型別轉換動作是某個刻意進行的其他作業所產生的較不明顯的副作用,就是『 隱含的強制轉型( implicit coercion )』
    看看例子:

      let a = 99;
    
      let b = a + '';   // '99' 隱含的強制轉型
    
      let c = String(a); // '99' 明確的強制轉型
    

    對 b 來說強制轉型是隱含的發生,因為 + 運算子的運算元,只要有一個是 string 值 ,就會堅持進行 string 的串接( concatenation 即將兩個字串加在一起 )

  • 明確與隱含都是相對的,如果你從未見過 string(..),那對你來說這就是隱含的

  • 不過要記得的是,程式碼很少只是寫給自己讀的,要考慮到團隊成員閱讀你的程式碼時會怎麼想,他們的明確與隱含是否和你一樣?

抽象的值運算

ToString

任何非 string 值被強制轉型為 string 表示值時,轉換過程由 2021 規格書 7.1.17 中的 Table 13 來處理
toString

  • 內建的基本型別值( primitive values )具有自然的字串化方式,null => 'null'、undefined => 'undefined'、true => 'true',而 number 如我們所預料的那樣表達

  • 但對正規的物件來說,除非指定了自己的方法,不然預設的 toString(..) ( Object.prototype.toString() ) 會回傳內部的 [[Classs]]( 內部分類 )

      let obj = {a:1};
    
      obj.toString(); // "[object Object]"
    
  • 陣列 ( array ) 有一個覆寫 toString() 的預設值,它會進行字串方式化的方式把陣列中所有的值( 先把每個都先字串化 )串接起來成為一個字串,其中每個值以 ',' 隔開

      let a = [4,5,6];
    
      a.toString(); // "4,5,6"
    
  • toString() 可以明確地被呼叫,或是當一個非 string 被用在 string 情境中,它會被自動呼叫

JSON 的字串化

  • 與 ToString 非常有關係的工作,發生在將一個值序列化 ( serialize ) 為一個 JSON 相容的 string 值

  • 有一個重點是,字串化不完全等同強制轉型,對於多數簡單的值,JSON 字串化的行為基本上與 toString() 轉換相同,只不過序列化後的結果永遠都是一個 string
    看看例子:

      JSON.stringify(99); // "99"
    
      JSON.stringify("99"); // "'99'" 一個內含帶有引號字串值的字串
    
      JSON.stringify(null); // "null"
    
      JSON.stringify(true); // "true"
    
      JSON.stringify({a:1}); // "{"a":1}"
    
  • 不是 JSON-safe 的值包括了 undefined、function、symbol 以及帶有循環參考的 object,主要是因為它們無法移植到也接受處理 JSON 值的其他程式語言

  • JSON.stringify(..) 遇到 undefined、function、symbol 值時會自動忽略它們

    • 如果在 array 裡面遇到,該值就會被取代為 null
    • 如果在 object 的特性碰到,該特性單純會被排除
    • 如果是帶有循環參考的 object 就會擲出錯誤

      JSON.stringify(undefined); // undefined
      
      JSON.stringify(function(){}); // undefined
      
      JSON.stringify([1,undefined,function(){},4]); // "[1,null,null,4]"
      
      JSON.stringify({a:1,b:function(){}}); // "{"a":1}"
      
  • JSON 字串化有種特殊行為,如果一個 object 值定義為 toJSON() 方法,此方法會被先呼叫,以取得一個用於序列化的值

      let z = {};
    
      let a = {
    
        b: 99,
    
        c: z,
    
        d: function(){}
    
      };
    
      z.e = a;
    
      JSON.stringify(a); // TypeError: Converting circular structure to JSON
    
      a.toJSON = function(){
    
        return { b: this.b };
    
      }
    
      JSON.stringify(a); // "{"b":99}"
    
  • toJSON() 應該回傳適當的正規值,JSON.stringify(..) 會處理字串化的過程;toJSON() 應該被解讀為『 to a JSON-safe value suitable for stringification 』轉為一個適合用於字串化的 JSON-safe 值,而非『 to a JSON string 』 轉為一個 JSON 字串

      let a = {
    
        val: [4,5,6],
    
        toJSON: function() {
    
          return this.val.slice(1);
    
        }
    
      };
    
      let b = {
    
       val: [4,5,6],
    
        toJSON: function() {
    
          return "["+ this.val.slice(1).join() + "]";
    
        }
    
      };
    
      JSON.stringify(a); // "[5,6]"
    
      JSON.stringify(b); // ""[5,6]""
    
  • JSON.stringify() 的第二個選擇性引數叫做 replacer ( 取代器 ),可以是一個 array 或是 function ,可自訂一個 object 的遞迴序列化,提供了一種過濾機制,決定應該包含哪些特性

    • 如果 relacer 是 array ,它應該是由幾個 string 所構成的一個 array ,其中每個字串都是一個特性名稱,這些特性被允許包含在 object 序列化過程中

      let a = {
      
          b: 99,
      
          c: '99',
      
          d: [4,5,6]
      
      };
      
      JSON.stringify(a,['b','c'])  // "{"b":42,"c":"42"}"  ( 只包含 ['b','c'] )
      
      JSON.stringify(a,['e']); // "{}"
      
    • 如果 relacer 是 function ,它會為 object 本身被呼叫一次,然後再為那個 object 中的每個特性被呼叫一次,每次都會有兩個引數,key & value 被傳入

      JSON.stringify(a,function(key,val) {
      
          if(key !== 'c') return val
      
      });
      
      // "{"b":99,"d":[4,5,6]}"
      
    • 第一次呼叫( a 物件本身被傳入時 )的 key 會是 undefined,字串化的過程是遞迴( recursive )的,所以 [4,5,6] 陣列每個值也都會作為 v 傳入給 replacer

      JSON.stringify(a,function(key,val) {
      
         console.log('key',key);
      
         console.log('val',val);
      
         return val;
      
      });
      
      //
      
      a 物件本身
      key 
      val {b: 99, c: "99", d: Array(3)}
      
      ------------------------------------------
      
        傳入 a 的每個特性
        key b
        val 99
      
        key c
        val 99
      
        key d
        val (3) [4, 5, 6]
      
      ------------------------------------------
            傳入 d: [4,5,6]
              key 0
              val 4
      
              key 1
              val 5
      
              key 2
              val 6
      
  • JSON.stringify() 的第三個選擇性引數叫做 space ( 空格 ),用於縮排以產生美觀易讀的輸出,可以是正整數 ( 即要用幾個空格字元),可以是 string ( 其值的前 10 個字元會被用於每個縮排層級 )

      JSON.stringify(a,null,3);
    
      //
      "{
    
         "b": 99,
    
         "c": "99",
    
         "d": [
    
            4,
    
            5,
    
            6
    
         ]
    
      }"
    
      ------------------------------------------
    
      JSON.stringify(a,null,'----+')
    
      //
      "{
    
      ----+"b": 99,
    
      ----+"c": "99",
    
      ----+"d": [
    
      ----+----+4,
    
      ----+----+5,
    
      ----+----+6
    
      ----+]
    
      }"
    
#type conversion #type coercion #ToString #JSON
「你所不知道的 JS 」系列書籍閱讀心得,未閱讀前對於 JavaScript 皆是懵懵懂懂,因面試時發現自己很多觀念都不正確不清楚,所以這次一探 JavaScript 的運作方式。 * 系列一開始會先把大方向簡短的整理,之後會以每個項目做詳細的筆記






Related Posts

如何做出一個好的 NodeJS 模組?

如何做出一個好的 NodeJS 模組?

從 V8 bytecode 探討 let 與 var 的效能問題

從 V8 bytecode 探討 let 與 var 的效能問題

Day2 android UI實作+activity介紹!!

Day2 android UI實作+activity介紹!!



Sponsored



Comments