开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情
JSON.stringify() 可以将对象或值转化为 JSON 字符串。理论上,它可以接受很多种不同的数据类型作为参数,而不同的数据类型,处理和转化的结果也不同。所以在实现这个方法之前,我们先弄清楚具体的处理规则。
不同数据类型的处理结果
先看基本数据类型:
| 数据类型 | 处理结果 | 数据类型 | 处理结果 |
|---|---|---|---|
| String | 返回'"string"' | Number | 返回 "1234"(NaN,±Infinity 返回 "null") |
| Null | 返回“null” | Undefined | 返回 undefined |
| Symbol | 返回 undefined | Boolean | 返回 "true"/"false" |
再看引用数据类型:
| 数据类型 | 处理结果 | 数据类型 | 处理结果 |
|---|---|---|---|
| 对象字面量 | 递归序列化。但是值为 undefined / Symbol / 函数类型的属性、类型为 Symbol 的属性会丢失 | 类数组对象 | 同对象字面量 |
| 基本类型的包装对象 | 一般返回包装对象的 valueOf(string 类型前后要加引号)的字符串形式,但是 Symbol 类型返回 "{}" | 数组 | 递归序列化。但是 undefined、Symbol、函数类型的属性会返回 "null" |
| Map | 返回 "{}" | Set | 返回 "{}" |
| Error | 返回 "{}" | RegExp | 返回 "{}" |
| Function | 返回 undefined | Date | 返回调用 toJSON 后生成的字符串 |
实现的思路
在接下来的代码实现中,首先会分为基本数据类型和引用数据类型两种情况:
-
基本数据类型:按照上面的规则返回序列化结果。重点处理 undefined 类型、symbol 类型以及 number 类型中的 NaN、±Infinity。
-
引用数据类型(按照是否可以继续遍历再分为两种):
- 可继续遍历的类型:包括对象字面量、数组、类数组对象、Set、Map。需要丢失的属性,在遍历时跳过即可。
- 不可继续遍历的类型:包括基本类型的包装对象、Error 对象、正则对象、日期对象函数。用一个函数集中进行处理
此外,在遍历数组或对象的时候,还需要检测是否存在循环引用的情况,若存在需要抛出相应的错误
数据类型判断
用 getType 获取具体的数据类型。因为对于基本类型 Symbol 和它的包装类型的处理方式不同,所以用 "Symbol_basic" 表示基本类型 Symbol,用 "Symbol" 表示它的包装类型。
function getType(o) {
return typeof o === "symbol"
? "Symbol_basic"
: Object.prototype.toString.call(o).slice(8, -1);
}
用 isObject 判断是引用类型还是基本类型:
function isObject(o){
return o !== null && (typeof o === 'object' || typeof o === 'function')
}
处理不可继续遍历的类型
用 processOtherTypes 处理所有不可继续遍历的引用类型:
function processOtherTypes(target,type){
switch(type){
case 'String':
return `"${target.valueOf()}"`
case 'Number':
case 'Boolean':
return target.valueOf().toString()
case 'Symbol':
case 'Error':
case 'RegExp':
return "{}"
case 'Date':
return `"${target.toJSON()}"`
case 'Function':
return undefined
default:
return “”
}
}
尤其需要注意 String 包装类型,不能直接返回它的 valueOf(),还要在前后加上引号。比如说 {a:"bbb"} ,我们期望的序列化结果应该是 '{a:"bbb"}',而不是 '{a:bbb}';同理,对于 Date 对象,直接返回它的 toJSON() 会得到 '{date: 1995-12-16T19:24:00.000Z}',但我们想得到的是 '{date: "1995-12-16T19:24:00.000Z"}',所以也要在前后加上引号。
检测循环引用
循环引用指的是对象的结构是回环状的,不是树状的:
// 下面的对象/数组存在循环引用
let obj = {};
obj.a = obj;
let obj1 = { a: { b: {} } };
obj1.a.b.c = obj1.a;
let arr = [1, 2];
arr[2] = arr;
// 注意这个对象不存在循环引用,只有平级引用
let obj2 = {a:{}};
obj2.b = obj2.a;
如何检测循环引用呢?
- 考虑最简单的情况,只有 key 对应的 value 为对象或者数组时,才可能存在循环引用,因此在遍历 key 的时候,判断 value 为对象或者数组之后才往下处理循环引用。
- 每一个 key 会有自己的一个数组用来存放父级链,并且在递归的时候始终传递该数组。如果检测到当前 key 对应的 value 在数组中出现过,则证明引用了某个父级对象,就可以抛出错误;如果没出现过,则加入数组中,更新父级链
所以一个通用的循环引用检测函数如下:
function checkCircular(target,parentArray = [target]){
Object.keys(target).forEach(key => {
if(typeof target[key] == 'object'){
if(parentArray.inlcudes(target[key])
|| checkCircular(target[key],[target[key],...parentArray])
){
throw new Error('存在循环引用')
}
}
})
console.log('不存在循环引用')
}
在 JSON.stringify 的实现中,遍历 key 的过程已经在主代码完成了,所以这里的 checkCircular 只需要包含检测过程。稍加改造如下:
function checkCircular(target,currentParent){
let type = getType(target)
if(type == 'Object' || type == 'Array'){
throw new TypeError('Converting circular structure to JSON')
}
currentParent.push(target)
}
核心代码
最终实现的核心代码如下:
function jsonStringify(target,initParent = [target]){
let type = getType(target)
let iterableList = ['Object','Array','Arguments','Set','Map']
let specialList = ['Undefined','Symbol_basic','Function']
// 如果是基本数据类型
if(!isObject(target)){
if(type === 'Symbol_basic' || type === 'Undefined'){
return undefined
} else if(Number.isNaN(target) || target === Infinity || target === -Infinity) {
return "null"
} else if(type === 'String'){
return `"${target}"`
}
return String(target)
}
// 如果是引用数据类型
else {
let res
// 如果是不可以遍历的类型
if(!iterableList.includes(type)){
res = processOtherTypes(target,type)
}
// 如果是可以遍历的类型
else {
// 如果是数组
if(type === 'Array'){
res = target.map(item => {
if(specialList.includes(getType(item))){
return "null"
} else {
// 检测循环引用
let currentParent = [...initParent]
checkCircular(item,currentParent)
return jsonStringify(item,currentParent)
}
})
res = `[${res}]`.replace(/'/g,'"')
}
// 如果是对象字面量、类数组对象、Set、Map
else {
res = []
Object.keys(target).forEach(key => {
// Symbol 类型的 key 直接略过
if(getType(key) !== 'Symbol_basic'){
let keyType = getType(target[key])
if(!specialList.includes(keyType)){
// 检测循环引用
let currentParent = [...initParent]
checkCircular(target[key],currentParent)
// 往数组中 push 键值对
res.push(
`"${key}":${jsonStringify(target[key],currentParent)}`
)
}
}
})
res = `{${res}}`.replace(/'/g,'"')
}
}
return res
}
}