基本概念
序列化
- 将json对象转换为字符串“序列”
JSON.stringify(value, replacer, space)
- 可通过修改对象的toJSON属性,来进一步控制
反序列化
- 将字符串“序列”转换为json对象
JSON.parse(text, reviver)
JSON.stringify()的特殊情况
- undefined、任意的函数以及 symbol 值在序列化过程中有两种情况
- 被忽略(出现在非数组对象的属性值中时)
- 被转换成 null(出现在数组中时)。
JSON.stringify({a:new Function}) // '{}' JSON.stringify([new Function]) // '[null]'
- 函数、undefined 被单独转换时,会返回 undefined。
JSON.stringify(function()) or JSON.stringify(undefined).
- Date 日期调用了 toJSON() 将其转换为了 string 字符串(同 Date.toISOString()),因此会被当做字符串处理。
- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
reviver的理解
一个出乎我意料的例子
粗略扫了遍文档,我以为我已经完全掌握了序列化和反序列化,但 JSON.parse() - JavaScript | MDN给了一个出乎我意料的例子:
const transformedObj1 = JSON.parse('[1,5,{"s":1}]', (key, value) => {
return typeof value === "object" ? undefined : value;
});
console.log(transformedObj1); // undefined
// const message = [1,2,3]
// console.log(typeof message) //object
按照我的理解,我以为最终结果会是[1,5]
。看来自己还是图样,于是请教了ChatGPT老师。
reviver函数的概念
执行顺序
JSON.parse
解析 JSON 字符串时,reviver
函数会以深度优先的顺序处理每个键值对。即它会先解析嵌套的对象或数组中的值,然后再解析外层对象或数组。key 和 value 的含义 key: 当前被处理的键(对于数组,key 是索引,最外层对象的 key 是空字符串 “")。 value: 当前被处理的值,这个值可以是一个基本数据类型(如字符串、数字)或复杂的对象、数组。
可以通过以下例子来理解:
const obj = JSON.parse('{"a": 1, "b": {"c": 2, "d": 3}, "e": [4, 5]}', (key, value) => {
console.log(`key: ${key}, value: ${value}`);
return value;
});
/*
key: a, value: 1
key: c, value: 2
key: d, value: 3
key: b, value: [object Object]
key: 0, value: 4
key: 1, value: 5
key: e, value: 4,5
key: , value: [object Object]
*/
回到刚才的例子
理解了reviver回调函数,我就对刚才的例子没有疑问了。
reviver函数里的这段代码执行了五次,而最后的key为""
,value则为[1,5,{"s":1}]
。因为在 JavaScript 中,数组(Array)是 object 的一种特殊类型,因此最终结果是undefined
而如果想要符合我一开始认为的结果,代码应该这样写:
const transformedObj1 = JSON.parse('[1,5,{"s":1}]', (key, value) => {
if(key!=="")
return typeof value === "object" ? undefined : value;
else
return value
});
console.log(transformedObj1);
JSON.parse(JSON.stringify(obj))
我第一次读到这行代码的时候还觉得奇怪,为什么要这样写,顺手就想改掉。想了下才意识到这是为了实现深拷贝。
优点
- 代码简单
- JSON.stringify内部做了循环引用的检测,正是我们上面提到破解循环引用
局限性
但这样写好吗?搜索后我发现有两篇文章将它的局限性写得很透彻了。
- javascript - 关于JSON.parse()和JSON.stringify()的性能小测试 - 超级有温度的代码 - SegmentFault 思否
- javascript - 关于 JSON.parse(JSON.stringify(obj)) 实现深拷贝的一些坑 - 超级有温度的代码 - SegmentFault 思否
我将其中的主要内容摘录如下:
- 只适用于一般数据的拷贝(结合上文,可以发现这里的问题主要来自JSON.stringify对特殊情况的处理)
1.如果json里面有时间对象,则序列化结果:时间对象=>字符串的形式; 2.如果json里有RegExp、Error对象,则序列化的结果将只得到空对象 RegExp、Error => {}; 3.如果json里有 function,undefined,则序列化的结果会把 function,undefined 丢失; 4.如果json里有NaN、Infinity和-Infinity,则序列化的结果会变成null; 5.如果json里有对象是由构造函数生成的,则序列化的结果会丢弃对象的 constructor; 6.如果对象中存在循环引用的情况也无法实现深拷贝
- 性能问题:
能不用JSON.parse()和JSON.stringify()就不用,采用替代方案且性能更优的。PS:特别是需要多次执行的代码块或是这个JSON数据比较庞大时
这里的第二点我深有体会,之前我便写过这样一段代码,它将一张100kb左右的图片转码为base64,塞进了json中,并序列化为字符串进行传输,接收端再将其反序列化后提取出来。后来重构代码时,这一段代码就成了提升性能的一大突破口。
替代方案
javascript - 深拷贝的终极探索(99%的人都不知道) - 颜海镜 - SegmentFault 思否