详细说一下 JSON.stringify 的一些特性?和遍历相比,哪个性能会更高?
JSON.stringify 的一些特性
常用的一些场合:
1、将 JSON object 存储到 localStorage 中;
2、POST 请求中的 JSON body
3、处理相应体重的 JSON 形式的数据
4、甚至某些条件下,我们还会用它来实现简单的深拷贝
5、……
对于 undefined、任意函数以及 symbol 三个特殊的值分别作为对象属性的值、数组元素、单独的值时,JSON.stringify() 将返回不同的结果
1、undefined、任意函数以及 symbol 作为对象属性值时, JSON.stringify() 将跳过(忽略)对它们进行序列化
const data = { a: 'a', b: undefined, c: Symbol("c"), fn: function(){ return 'fn'; }, }; JSON.stringify(data); // {a: a}
2、undefined、任意函数以及 symbol 作为数组元素时,JSON.stringify() 会将它们序列化为 null
JSON.stringify([ 'a', undefined, function fn(){ return "fn"; }, Symbol("c"), ]}; // "["a", null, null, null]"
3、undefined、任意函数以及 symbol 被 JSON.stringify() 作为单独的值进行序列化时都会返回 undefined。
JSON.stringify(function a(){ console.log("a"); }); // undefined JSON.stringify(undefined); // undefined JSON.stringify(Symbol("c")); // undefined
非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
需要注意的是,因为 1.1 中,JSON.stringify 序列化时会忽略一些特殊的值。所以不能保证序列化的字符串还是以特定的顺序出现(数组除外)
const data = { a: 'a', b: undefined, c: Symbol("c"), fn:function (){ return "fn"; }, d: "d", }; JSON.stringify(data); // "{"a": "a", "d":"d"}" JSON.stringify([ "a", undefined, function fn(){ return "fn" }, Symbol("c"), (d: "d"), ]); //"["a", null, null, null, "d"]"
转换值时如果有 to.JSON() 函数,该函数返回什么值,序列化结果就是什么值,并且忽略其他属性的值。
JSON.stringify({ str: "str", toJSON: function () { return "strToJson"; }, }); // "strToJson"
JSON.stringify() 将会正常序列化 Date 的值
JSON.stringify({now: new Date()}); // "{"now":"2021-02-01T05:00:54.082Z"}"
实际上Date 对象自己部署了 toJSON() 方法,因此 Date 对象会被当做字符串处理。
NaN 和 infinity 格式的数值及 null 都会被当作 null
JSON.stringify(NaN); // "null" JSON.stringify(null); // "null" JSON.stringify(Infinity); // "null"
布尔值、数字、字符串的包装对象在序列化过程中会自动转化成对应的原始值
JSON.stringify([new Number(1), new String("false"), new Boolean(false)]); // "[1,"false",false]"
其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性
// 不可枚举的属性默认会被忽略: JSON.stringify( Object.create(null, { x: { value: "json", enumerable: false }, y: { value: "stringify", enumerable: true }, }) ); // "{"y":"stringify"}"
实现深拷贝最简单粗暴的方式就是序列化:JSON.parse(JSON.stringify(obj)),这个方式实现深拷贝会因为序列化的诸多特性从而导致诸多的坑点:比如循环引用的问题
// 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。 const obj = { name: "loopObj", }; const loopObj = { obj, }; // 对象之间形成循环引用,形成闭环 obj.loopObj = loopObj; // 封装一个深拷贝的函数 function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } // 执行深拷贝,抛出错误 deepClone(obj); /** VM44:9 Uncaught TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'loopObj' -> object with constructor 'Object' --- property 'obj' closes the circle at JSON.stringify (<anonymous>) at deepClone (<anonymous>:9:26) at <anonymous>:11:13 */
对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。这也就是为什么用序列化去实现深拷贝时会遇到循环引用的对象会抛出错误的原因、
所有以 symbol 为属性键的属性都会被完全忽略掉
JSON.stringify({ [Symbol.for("key")]: "json" }); // {}
当尝试去转换 BigInt 类型的值会抛出 TypeError(BigInt 值不能 JSON 序列化)
const data = { num: BigInt(10), }; JSON.stringify(data); // Uncaught TypeError: Do not know how to serialize a BigInt
JSON.stringify 第二个参数 replacer
第二个参数可以是一个函数或者一个数组。作为函数时,它有两个参数,键(key)和值(value),函数类似就是数组的方法 map、filter 等方法的回调函数,对每一个属性值都会执行一次该函数。如果是一个数组,数组的值代表将被序列化成 JSON 字符串的属性名
作为函数
可以打破上面特性中的大多数特性
const data = { a: "a", b: undefined, c: Symbol("c"), fn: function () { return "fn"; }, }; JSON.stringify(data, (key, value) => { switch (true) { case typeof value === "undefined": return "undefined"; case typeof value === "symbol": return value.toString(); case typeof value === "function": return value.toString(); default: break; } return value; }); // "{"a":"a","b":"undefined","c":"Symbol(c)","fn":"function () {\n return \"fn\";\n }"}"
但是需要注意的是,第二个参数作为函数时,传入函数的第一个参数不是对象的第一个键值对,而是空字符串作为 key 值,value 值为整个对象的键值对。
const data = { a: 1, b: 2, }; JSON.stringify(data, (key, value) => { console.log(key, "-", value); return value; }); // - {a: 1, b: 2} // a - 1 // b - 2
第二个参数作为数组
数组的值就代表了将被序列化成 JSON 字符串的属性名
const data = { a: 1, b: 2, }; JSON.stringify(data, ["a"]); // "{"a":1}" JSON.stringify(data, ["b"]); // "{"b":2}" JSON.stringify(data, ["a", "b"]); // "{"a":1,"b":2}"
JSON.stringify 第三个参数 space
指定缩进用的空白字符串,用于美化输出;如果参数是个数字,他代表有多少的空格;上限为 10。该值若小于 1,则意味着没有空格;如果该参数为字符串(当字符串长度超过 10 个字母,取其前 10 个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。
const data = { a: 1, b: 2, }; JSON.stringify(data, null, 2); /* "{ "a": 1, "b": 2 }" */
JSON.stringify 和遍历比较
const map1 = {}; const map2 = {}; for (let i = 0; i < 1000000; i++) { map1[i] = i; map2[i] = i; } function f1() { console.time("jsonString"); const start = new Date().getTime(); const r = JSON.stringify(map1) == JSON.stringify(map2); console.timeEnd("jsonString", r); } function f2() { console.time("map"); const r = Object.keys(map1).every((key) => { if (map2[key] || map2[key] === 0) { return true; } else { return false; } }); console.timeEnd("map", r); } f1(); f2(); // jsonString: 540.698974609375ms // map: 124.853759765625ms
可以看到 JSON.stringify 和遍历比较的时间相差近 5 倍的时间,其实用 json 的 api 底层也是遍历,并且转成字符串,所以性能会比较差。