深刻理解JSON.stringify()

image

就我目前4年(實習了1年,965了1年,996了2年,算3年感受少了,說是4年老司機也不爲過吧。)的工做經驗來看,JSON.stringify通常有如下用途:javascript

  • 深拷貝:深拷貝引用類型的數據
  • 序列化:服務端存儲重依賴前端的數據,localStorage/sessionStorage存儲的數據

但其實除了上面兩種經常使用的用法以外,JSON.stringify還有許多更增強大的特性,值得系統性學習。
關鍵是這些特性多是你開發過程當中常常用到的,只是你不知道而已前端

也許你以爲這是一篇枯燥的長篇大論的講用法的博文,那麼我就說幾個有趣的場景吧:vue

  • 爲何這個對象明明有foo屬性,序列化後在數據庫持久化存儲以後,這個屬性咋就沒了呢?
  • 爲何axios發送post請求時,若是req的body包含undefined值的參數,在發給服務端的請求中會消失?
  • 打印出的JSON字符串就這麼不易讀麼?出了store成一個variable而後parse以外,能不能直接parse就清晰看到它的數據結構呢?
  • 爲何序列化一個對象,它還報錯呢?報的仍是那種本身引用本身的錯誤?

若是這幾個問題看了以後是一臉懵逼,那麼必定要細細閱讀一下這篇博文,答案就在文中。聰明的你必定會找到答案的。java

  • 工做中經常使用JSON.stringify()ios

    • 深拷貝:深拷貝引用類型的數據(JSON.parse(JSON.stringify(obj/arr)))
    • 序列化:服務端存儲重依賴前端的數據, localStorage/sessionStorage存儲的數據(例如fabric.js的canvas模板數據,vue-amap的svg路徑信息等等git

      • 存儲重前端功能的數據
      • localStorage/sessionStorage存儲的數據
      • 查詢JSON數據作解析
  • 初識JSON.stringify()github

    • 序列化數據
    • 過濾數據
    • 格式化數據
  • JSON.stringify(value, [ ,replacer[ ,space]]) 語法數據庫

    • 參數canvas

      • value
      • replacer(可選)
      • space(可選)
    • 返回值
    • 異常axios

      • TypeError 「cyclic object value」
      • TypeError 「BigInt value can't be serialized in JSON」
  • JSON.stringify()描述

    • 常見使用須知
    • replacer 參數

      • 參數爲函數
      • 參數爲數組
    • space 參數

      • 參數爲number
      • 參數爲string
    • toJSON()的表現
  • JSON.stringify()序列化循環引用時的問題

    • 自引用對象拋出TypeError
    • JSON.stringify高德地圖vue-amap中的自引用對象
    • 如何序列化自引用對象?
  • JSON.stringify()序列化具備相同屬性相同值但屬性順序不一樣的對象,結果相等嗎?
  • JOSN.stringify()與localStorage的使用示例
  • 回答一下文章開頭的問題

工做中經常使用JSON.stringify()

深拷貝:深拷貝引用類型的數據(JSON.parse(JSON.stringify(obj/arr)))

const obj = {
    foo: 'hi',
    bar: {
        name: 'bar',
        age: 3
    },
    baz: ['hello','javascript']
}
const arr = ['hi', { name: 'bar', age: 3 }, ['hello','javascript']];
const deepCopy = JSON.parse(JSON.stringify(obj/arr))

深拷貝以後,deepCopy會生成一個內存獨立的obj或者arr。
也就是說obj/arr與deepCopy存儲在不一樣的堆內存,修改obj/arr不會影響deepCopy,修改deepCopy也不會影響obj或者arr。

如果對於深淺拷貝不理解,建議先找資料系統性學習一下。

序列化:服務端存儲重依賴前端的數據,localStorage/sessionStorage存儲的數據

服務端存儲重依賴前端的數據:例如fabric.js的canvas模板數據,vue-amap的svg路徑信息等等。
localStorage/sessionStorage存儲的數據: LocalStorage/SessionStorage The keys and the values are always strings。

存儲重前端功能的數據

例如Canvas,SVG信息,服務端作持久化。

const complexFabricJSCanvasTemplate = { ... };
const complexVueAMapSVGObject = { ... };
const JSONStr = JSON.stringify(complexFabricJSCanvasTemplate/complexVueAMapSVGObject);
axios.post('/create', {
    name: 'fabric.js', // "vue-amap"
    data: JSONStr,
  })
  .then((res)=>{
    console.log(res);
  })
  .catch((err)=>{
    console.log(err);
  });
localStorage/sessionStorage存儲的數據
const testObj = {foo: 1, bar: 'hi', baz: { name: 'frankkai', age: 25 }}
localStorage.setItem('testObj', JSON.stringify(testObj ));

若不轉化,也不會報錯,會致使存儲失效:localStorage.getItem('testObj');// "[object Object]"
image

查詢JSON數據作解析

從服務端接口查詢到存儲好的Canvas或SVG數據,作解析後傳入到fabric.js,vue-amap等進行繪製。

return new Promise((resolve)=>{
    axios.get('/retrive', {
         name: 'fabric.js', // "vue-amap"
    })
    .then((res)=>{
      const complexFabricJSCanvasTemplate = JSON.parse(res.result);
      const complexVueAMapSVGObject = JSON.parse(res.result);
      resolve(complexFabricJSCanvasTemplate/complexVueAMapSVGObject);
    })
    .catch((err)=>{
      console.log(err);
    });
})

初識JSON.stringify()

  • 序列化數據 JSON.stringify()方法將一個js對象或者值轉換爲JSON string類型。
  • 過濾數據 若是指定replacer爲數組或函數,能夠對數據進行過濾。
  • 格式化數據 若是指定space能夠對JSON的string進行格式化。

序列化數據

console.log(JSON.stringify({ x: 5, y: 6 }));
// expected output: "{"x":5,"y":6}"

console.log(JSON.stringify([new Number(3), new String('false'), new Boolean(false)]));
// expected output: "[3,"false",false]"

console.log(JSON.stringify({ x: [10, undefined, function(){}, Symbol('')] }));
// expected output: "{"x":[10,null,null,null]}"

console.log(JSON.stringify(new Date(2006, 0, 2, 15, 4, 5)));
// expected output: ""2006-01-02T15:04:05.000Z""

過濾數據

var foo = {foundation: 'Mozilla', model: 'box', week: 45, transport: 'car', month: 7};
// replacer爲數組
JSON.stringify(foo, ['week', 'month']);  // '{"week":45,"month":7}', 只返回week"和"month"
// replacer爲函數
function replacer(key, value) {
  if (typeof value === 'string') return undefined;
  return value;
}
JSON.stringify(foo, replacer); // '{"week":45,"month":7}',返回callback不爲undefined的。

格式化數據

首行縮進兩個空格。

    • 「 」 兩個空格
    • 數字2
    JSON.stringify({ a: 2 }, null, '  ');
    // JSON.stringify({a:2}, null, 2)

    =>

    "{
      "a": 2
    }"

    JSON.stringify(value, [ ,replacer[ ,space]]);語法

    JSON.stringify(value, [ ,replacer[ ,space]]);

    參數

    value

    轉化爲JSON string的值。

    replacer

    過濾數據。

    函數:replacer能夠是函數,返回undefined時不輸出數據,非undefined的數據被輸出。
    字符串數組:replacer能夠是數組,通常是string,也能夠是number。指定輸出JSON的屬性白名單。['week', 'month']
    null或不寫:replacer爲null或者不寫時,全部屬性都被輸出。null的話通常用於不過濾數據僅設置space的狀況。

    疑惑:replacer的數組中是數字?[1,2,3,4,5] ?

    const arr = { 999: 'hi', foo: 'js', bar:' java' };
    JSON.stringify(arr, [999, 'foo']); // 打印出"{"999":"hi","foo":"js"}"。 這裏的999是number類型。
    space

    加強可讀性的縮進空格或填充字符串。

    • 空格鎖進數 string或number 指明須要插入幾個空格。
    • number最大最小值 空白數。number最大值爲10,大於10時也取10。小於1時則表明不須要縮進。
    • string最大最小值 空白數。string的前10個字符,大於10只取前10個。
    • null或不寫時不縮進 值爲null或不寫,則默認不鎖進。

    返回值

    JSON string。

    異常

    TypeError 「cyclic object value」

    序列化自引用的對象時會報這個錯。高德地圖的segments就是自引用對象。如何序列化自引用的對象可見下文。
    image

    TypeError 「BigInt value can't be serialized in JSON」

    包含突破Number存儲上線的BitInt類型的obj,不能被序列化。

    JSON.stringify({foo: 1n}) // TypeError 「BigInt value can't be serialized in JSON」

    JSON.stringify()描述

    常見使用須知

    若是想更好的使用JSON.stringify(),下面這些使用須知必定要掌握。

    • toJSON方法 若是被序列化的value有toJSON()方法,能夠在其中定義數據是如何序列化的。
    • 數據格式保持 序列化後,Boolean,Number,String的數據類型會保留,這種轉換是符合傳統語義的。
    • 不支持轉化undefined,function,Symbol 這些類型不是有效的JSON值。若是是普通的,直接被忽略;若是在數組中找到時,直接被轉成null。 JSON.stringify(function(){}) 或JSON.stringify(undefined)返回undefined。JSON.stringify({ foo: Symbol('foo') });// "{}"
    • Symbol類型做爲key時,會被徹底忽略。 即便在replacer中明確指明。JSON.stringify({ [Symbol('foo')]: 'foo' });// '{}' JSON.stringify({ [Symbol.for('foo')]: 'foo' }, [Symbol.for('foo')]);// '{}'
    • Date類型做爲值時。 Date的toJSON方法等同於date.toISOString()。
    • 特殊的類型:Infinity,NaN, undefined, null Infinity,NaN, undefined, null都被看成null。
    • 特殊的類型:Map,Set, WeakMap,WeakSet 全部其餘的Object實例(包括Map,Set,WeakMap,WeakSet)都僅有enumerable屬性能被序列化。這些值默認是enumerable爲false的。var foo = [['foo',1]];var mp = new Map(foo);JSON.stringify(mp); // "{}"
    • 數組的string類型屬性是不可被enumerable(列舉)的。 let a = ['foo', 'bar']; a['baz'] = 'quux'; // a: [ 0: 'foo', 1: 'bar', baz: 'quux' ]; JSON.stringify(a); // '["foo","bar"]'
    • TypedArray類型能夠序列化。 JSON.stringify([new Float32Array([1]), new Float64Array([1])]);// '[{"0":1},{"0":1}]'

    究極使用示例:

    JSON.stringify({});                    // '{}'
    JSON.stringify(true);                  // 'true'
    JSON.stringify('foo');                 // '"foo"'
    JSON.stringify([1, 'false', false]);   // '[1,"false",false]'
    JSON.stringify([NaN, null, Infinity]); // '[null,null,null]'
    JSON.stringify({ x: 5 });              // '{"x":5}'
    
    JSON.stringify(new Date(2006, 0, 2, 15, 4, 5)) 
    // '"2006-01-02T15:04:05.000Z"'
    
    JSON.stringify({ x: 5, y: 6 });
    // '{"x":5,"y":6}'
    JSON.stringify([new Number(3), new String('false'), new Boolean(false)]);
    // '[3,"false",false]'
    
    // String-keyed array elements are not enumerable and make no sense in JSON
    let a = ['foo', 'bar'];
    a['baz'] = 'quux';      // a: [ 0: 'foo', 1: 'bar', baz: 'quux' ]
    JSON.stringify(a); 
    // '["foo","bar"]'
    
    JSON.stringify({ x: [10, undefined, function(){}, Symbol('')] }); 
    // '{"x":[10,null,null,null]}' 
    
    // Standard data structures
    JSON.stringify([new Set([1]), new Map([[1, 2]]), new WeakSet([{a: 1}]), new WeakMap([[{a: 1}, 2]])]);
    // '[{},{},{},{}]'
    
    // TypedArray
    JSON.stringify([new Int8Array([1]), new Int16Array([1]), new Int32Array([1])]);
    // '[{"0":1},{"0":1},{"0":1}]'
    JSON.stringify([new Uint8Array([1]), new Uint8ClampedArray([1]), new Uint16Array([1]), new Uint32Array([1])]);
    // '[{"0":1},{"0":1},{"0":1},{"0":1}]'
    JSON.stringify([new Float32Array([1]), new Float64Array([1])]);
    // '[{"0":1},{"0":1}]'
     
    // toJSON()
    JSON.stringify({ x: 5, y: 6, toJSON(){ return this.x + this.y; } });
    // '11'
    
    // Symbols:
    JSON.stringify({ x: undefined, y: Object, z: Symbol('') });
    // '{}'
    JSON.stringify({ [Symbol('foo')]: 'foo' });
    // '{}'
    JSON.stringify({ [Symbol.for('foo')]: 'foo' }, [Symbol.for('foo')]);
    // '{}'
    JSON.stringify({ [Symbol.for('foo')]: 'foo' }, function(k, v) {
      if (typeof k === 'symbol') {
        return 'a symbol';
      }
    });
    // undefined
    
    // Non-enumerable properties:
    JSON.stringify( Object.create(null, { x: { value: 'x', enumerable: false }, y: { value: 'y', enumerable: true } }) );
    // '{"y":"y"}'
    
    
    // BigInt values throw
    JSON.stringify({x: 2n});
    // TypeError: BigInt value can't be serialized in JSON

    replacer 參數

    replacer能夠是function,也能夠是array。

    function
    function replacer(key="", value) {
        return value;
    }

    做爲function,有兩個參數key 和 value。
    默認狀況下replacer函數的key爲「」,對於對象中的每一個值,都會call一次這個函數。

    `function replacer(key,value){ 
        return value;
    }
    JSON.stringify({foo: false,bar:123},replacer);`

    返回值有如下幾種狀況:

    • number number相關聯的string屬性被添加。 return 123; // "123"
    • string 相關聯的string屬性被添加。return "foo"; // "foo"
    • boolean 「true」或」false「被添加。return true; // "true"
    • null "null"被添加。 return null; //"null"
    • value 若是是正常的value對象,那麼會遞歸它的屬性序列化成JSON string。// return value;"{"foo":false,"bar":123}"
    • undefined 不返回這個屬性。// JSON.stringify({foo: false,bar:undefined},replacer);"{"foo":false}"。

    使用強大的undefined是須要注意:

    1. undefined 用過axios的同窗這裏破案了:當咱們的post請求的reqBody包含以undefined爲值的參數時,根本不會發到服務端的接口,而null能夠。這就是JSON.stringify()的功勞。

    v = JSON.stringify(v)時,會將undefined類型的值自動過濾掉。

    // axios源碼 buildURL.js 37~56行:
      utils.forEach(params, function serialize(val, key) {
          if (utils.isArray(val)) {
            key = key + '[]';
          } else {
            val = [val];
          }
    
          utils.forEach(val, function parseValue(v) {
            if (utils.isDate(v)) {
              v = v.toISOString();
            } else if (utils.isObject(v)) {
              v = JSON.stringify(v); // 注意這裏
            }
            parts.push(encode(key) + '=' + encode(v));
          });
        });
    1. 不能用replacer去移除array的值。若返回undefined,會返回null。
    function replacer(key,value){
        if(typeof value ==='boolean'){return undefined}
        return value;
    }
    JSON.stringify([1, 'false', false],replacer);
    // "[1,"false",null]"
    經常使用方式
    function replacer(key, value) {
      // 返回undefined過濾屬性
      if (typeof value === 'string') {
        return undefined;
      }
      return value;
    }
    
    var foo = {foundation: 'Mozilla', model: 'box', week: 45, transport: 'car', month: 7};
    JSON.stringify(foo, replacer); // '{"week":45,"month":7}'
    JSON.stringify(foo, ['week', 'month']);  // '{"week":45,"month":7}',

    space 參數

    • 能夠是number 最大10,10個縮進
    • 能夠是string 最長取前10個,10個char縮進
    JSON.stringify({ a: 2 }, null, ' ');
    // '{
    //  "a": 2
    // 
    JSON.stringify({ uno: 1, dos: 2 }, null, '\t');
    // returns the string:
    // '{
    //     "uno": 1,
    //     "dos": 2
    // }'

    toJSON()的表現

    若是要被字符串化的對象具備一個名爲toJSON的屬性,其值是一個函數,則該toJSON()方法將自定義JSON字符串化行爲:代替被序列化的對象,該toJSON()方法返回的值將被序列化,而不是被序列化的對象。JSON.stringify()調用toJSON一個參數:

    • 若是此對象是屬性值,則返回屬性名稱
    • 若是它在數組中,則返回數組中的索引(做爲字符串)
    • 若是JSON.stringify()直接在此對象上調用,則返回一個空字符串
    var obj = {
        data: 'data',
        toJSON (key) {
            return key;
        }
    };
    
    JSON.stringify(obj);
    // '""""
    JSON.stringify({ obj })
    // '{"obj":"'obj'"}'
    JSON.stringify([ obj ])
    // '["'0'"]'

    JSON.stringify()序列化循環引用時的問題

    自引用對象拋出TypeError

    TypeError: Converting circular structure to JSON

    const circularReference = {};
    circularReference.myself = circularReference;
    // Serializing circular references throws "TypeError: cyclic object value"
    JSON.stringify(circularReference);

    image

    JSON.stringify高德地圖vue-amap中的自引用對象

    路徑規劃plans的路徑SVG信息segments對象。

    需求是這樣的,前端須要將路線的信息傳遞給後端,其中包括經緯度數組和SVG數組。
    用JSON.stringify()序列化經緯度數組是ok的,可是序列化的SVG數組是自引用的,會報錯。(是後來才知道這個SVG數組能夠不往服務端存的,不過當時序列化報錯是真的懵逼了)

    image

    如何序列化自引用對象?

    使用Douglas Crockford的cycle.js

    它在全局JSON對象上新增了2個方法:

    • JSON.decycle(將自引用的屬性myself:obj 替換爲myself:{$ref: "$"}
    • JSON.retrocycle

    使用示例:

    var circularReference = {};
    circularReference.myself = circularReference;
    JSON.decycle(circularReference);
    // { "$ref": "$" }

    JSON.stringify()序列化具備相同屬性相同值但屬性順序不一樣的對象,結果相等嗎?

    不相等。

    var a = JSON.stringify({ foo: "bar", baz: "quux" })
    //'{"foo":"bar","baz":"quux"}'
    var b = JSON.stringify({ baz: "quux", foo: "bar" })
    //'{"baz":"quux","foo":"bar"}'
    console.log(a === b) // false

    JOSN.stringify()與localStorage的使用示例

    localStorage只能存儲string類型的數據,所以須要使用JSON.stringify()將對象序列化爲JSON string。

    var session = {
      'screens': [],
      'state': true
    };
    session.screens.push({ 'name': 'screenA', 'width': 450, 'height': 250 });
    session.screens.push({ 'name': 'screenB', 'width': 650, 'height': 350 });
    session.screens.push({ 'name': 'screenC', 'width': 750, 'height': 120 });
    localStorage.setItem('session', JSON.stringify(session));
    var restoredSession = JSON.parse(localStorage.getItem('session'));
    console.log(restoredSession);

    回答一下文章開頭的問題

    爲何這個對象明明有foo屬性,序列化後在數據庫持久化存儲以後,這個屬性咋就沒了呢?

    值爲undefined?
    replacer對它作過濾了?
    toJSON方法中作過濾了?
    屬性的enumerable值爲false?
    Symbol?
    Map?Set?WeakMap?WeakSet?
    找緣由吧。

    爲何axios發送post請求時,若是req的body包含undefined值的參數,在發給服務端的請求中會消失?

    v = JSON.stringify(v); // v = {foo: undefined} =>"{}"

    源碼在buildURL.js 37~56行

    打印出的JSON字符串就這麼不易讀麼?出了store成一個variable而後parse以外,能不能直接parse就清晰看到它的數據結構呢?

    const testObj = {foo: 1, bar: 'hi', baz: { name: 'frankkai', age: 25 }}
    JSON.stringify(testObj, null, 4);

    =>

    "{
        "foo": 1,
        "bar": "hi",
        "baz": {
            "name": "frankkai",
            "age": 25
        }
    }"

    爲何序列化一個對象,它還報錯呢?報的仍是那種本身引用本身的錯誤?這種對象不能序列化了嗎?

    這是由於自引用以後,會限制死循環。
    引擎應該是作了特殊的處理,發現這種無限循環時自動拋出異常。
    這種對象不能序列化了嗎?可使用cycle.js的decycle(序列化。

    var circularReference = {};
    circularReference.myself = circularReference;
    JSON.decycle(circularReference);
    // { "$ref": "$" }

    參考資料:

    期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

    努力成爲優秀前端工程師!
    相關文章
    相關標籤/搜索