JSON.stringify() 实现原理
JSON 的语法可以表示以下三种类型的值。
- 简单值:使用与 JavaScript 相同的语法,可以在 JSON 中表示字符串、数值、布尔值和 null。但 JSON 不支持 JavaScript 中的特殊值 undefined。
- 对象:对象作为一种复杂数据类型,表示的是一组无序的键值对儿。而每个键值对儿中的值可以是简单值,也可以是复杂数据类型的值。
- 数组:数组也是一种复杂数据类型,表示一组有序的值的列表,可以通过数值索引来访问其中的值。数组的值也可以是任意类型——简单值、对象或数组。
JSON 不支持变量、函数或对象实例,它就是一种表示结构化数据的格式,虽然与 JavaScript 中表示数据的某些语法相同,但它并不局限于 JavaScript 的范畴。
let myObj = { undef: undefined, bool: false, fun: function(){}, date: new Date(), arr: [1, 2], obj: {a: 1, b: 2}, reg: /\d/, sym: Symbol(), nul: null, set: new Set(), map: new Map() } console.log(JSON.stringify(myObj)) // {"bool":false,"date":"2020-05-27T03:22:47.587Z","arr":[1,2],"obj":{"a":1,"b":2},"reg":{},"nul":null,"set":{},"map":{}}
这个例子使用 JSON.stringify()
把一个 JavaScript 对象序列化为一个JSON 字符串,现在我们已经了解了 JSON.stringify()
方法的输出以及它的工作方式,让我们从序列化值开始实现它。
序列化值,首先,我们将从以下数据类型开始。
- undefined
- number
- boolean
- string
function stringify(value) { // 参数类型 var type = typeof value; function getValues(value) { if (type === "undefined") { return undefined; } if (type === "number" || type === "boolean") { return "" + value + ""; } if (type === "string") { return '"' + value + '"'; } } return getValues(value); } console.log(stringify(1)); // "1" console.log(stringify("abc")); // ""abc"" console.log(stringify(true)); // "true" // 这里是 undefined 而不是 "undefined" console.log(stringify(undefined) === JSON.stringify(undefined)); // true
到目前为止,上述功能是比较简单。它所做的只是用引号把值引起来,但是 undefined 并不需要转换为字符串,而是直接返回 undefined 数据类型。
现在我们将添加对更多数据类型的支持,例如
- array
- object
- null
- date
- functions (methods)
为了支持数组和对象,我们应该解析属性之间的多层嵌套。我们必须递归地处理子元素并序列化值。
对于数组而言,它非常简单,请使用一个开括号和一个闭括号对数组进行迭代,然后调用该 stringify()
函数,然后依次调用该 getValues()
函数并重复进行,直到所有值都考虑在内。
但是对于对象,我们需要使用对象字面量的左,右括号将值和属性的序列化。
对于日期对象,还有一件有趣的事情,JSON.stringify()
方法返回的值为 ISO 8601 日期字符串(与在 Date 对象上调用 toISOString()
的结果完全一样)。
function stringify(value) { var type = typeof value; function getValues(value) { if (type === "undefined" || type === "function") { return undefined; } if (type === "number" || type === "boolean") { return "" + value + ""; } if (type === "string") { return '"' + value + '"'; } } // 对于对象数据类型 // 在javascript中,数组和对象都是对象 if (type === "object") { // 检查值是否为null if (!value) { return "" + value + ""; } // 检查值是否为日期对象 if (value instanceof Date) { return '"' + new Date(value).toISOString() + '"'; // 返回ISO 8601日期字符串 } // 检查值是否为Array if (value instanceof Array) { return "[" + value.map(stringify) + "]"; // 递归调用stringify函数 } else { // 否则它只是一个对象 // 递归调用stringify函数 return ( "{" + Object.keys(value).map( key => '"' + key + '"' + ":" + stringify(value[key]) ) + "}" ); } } return getValues(value); } console.log(stringify([1, 2, 3])); // "[1,2,3]" console.log(stringify(new Date())); // prints date in ISO format console.log(stringify({ a: 1 })); // ""{a:1}"" console.log(stringify(myObj) === JSON.stringify(myObj)); // true console.log(JSON.parse(stringify(myObj))); // 类似 JSON.parse(JSON.stringify(myObj))
上面的函数现在适用于所有数据类型,并且输出与 JSON.stringify()
方法相同。
实际上,JSON.stringify()
除了要序列化的 JavaScript 对象外,还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化 JavaScript 对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在 JSON 字符串中保留缩进。单独或组合使用这两个参数,可以更全面深入地控制JSON 的序列化。(这里实现的代码暂时不支持者两个参数)
与 JavaScript 不同,JSON 中对象的属性名任何时候都必须加双引号。手工编写 JSON 时,忘了给对象属性名加双引号或者把双引号写成单引号都是常见的错误。
如果对象中有 undefined,那么相应的属性会被忽略。
现在你已经了解了 JSON 在格式化数据的时候转换过程是怎样的,因为 JSON 对有些数据类型是不支持的,所以不建议大家使用 JSON 进行对象的深度克隆
JSON.parse(JSON.stringify(obj))
如果你需要用到深度克隆,可以参考下面的写法:
function deepClone (obj, hash = new WeakMap()) { if (obj == null) return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); if (typeof obj === 'symbol') { let desc = obj.description return desc ? Symbol(desc) : Symbol() } if (typeof obj !== 'object') return obj; if (hash.has(obj)) return hash.get(obj); let cloneObj = new obj.constructor; hash.set(obj, cloneObj); for (const key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = obj[key]; } } return cloneObj; }
上面代码使用了 WeakMap 数据结构来解决循环引用的问题,使用 JSON 拷贝循环引用的对象是会报错的,我们先了解一下什么是循环引用,也就是对象的某个属性引用对象本身。
let obj = { a: 1 } obj.b = obj
由于 WeakMap 只接受对象作为键名,我们可把拷贝之前的对象作为键名,拷贝之后的对象作为键值,调用 WeakMap 的 get 方法读取对象键名,如果存在说明这个对象发生了循环引用,然后直接返回键值就是拷贝之后的对象,而不用再次递归。
关于 WeakMap 知识你得先了解其他几种数据结构:
Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
Map
JavaScript 的对象(Object),本质上是键值对的集合,但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是 键 的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,对象 Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应。
WeakMap
WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。WeakMap 与 Map 的区别有两点。
首先,WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。其次,WeakMap 的键名所指向的对象,不计入垃圾回收机制。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论