共计 6889 个字符,预计需要花费 18 分钟才能阅读完成。
这是 JS 原生办法原理探索系列的第八篇文章。本文会介绍如何实现
JSON.stringify()
办法。
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
}
}
基本上依照下面表格中的规定来解决就行了,有几个细节能够留神一下:
iterableList
用于寄存能够持续遍历的数据类型;specialList
用于寄存比拟非凡的Undefined
、Symbol_basic
、Function
三种类型,非凡在于:对象 key 的 value 如果是这些类型,则序列化的时候会失落,数组的元素如果是这些类型,则序列化的时候会对立转化为"null"
。因为这三种类型要屡次用到,所以先存起来。-
为什么要将最终返回的
res
初始化为一个空数组?因为:- 如果咱们解决的
target
是数组,则只须要调用map
就能够将数组的每一个元素映射为序列化之后的后果,调用后返回的数组赋给res
,再和[
、]
字符拼接,会隐式调用数组的toString
办法,产生一个规范的序列化后果; - 如果解决的
target
是对象字面量,则能够将它的每个 key-value 的序列化后果 push 到res
中,最终再和{
、}
字符拼接,也同样会产生一个规范的序列化后果。 - 在整个过程中不须要去解决 JSON 字符串中的逗号分隔符。
- 如果咱们解决的
- 对于对象字面量,类型为
"Symbol_basic"
的属性会失落,属性值为Undefined
、Symbol_basic
、Function
三种类型的属性也会失落。属性失落其实就是在遍历对象的时候略过这些属性 - 在检测循环援用的时候,存在嵌套关系的对象应该共享同一条父级链,所以递归的时候须要把寄存父级链的数组传进去;同时,不存在嵌套关系的两个对象不应该共享同一条父级链(否则会将所有相互援用的状况都误认为是循环援用),所以每次遍历对象 key 的时候,都会从新生成一个
currentArray
。 - 最初,为保险起见,记得将序列化后果中可能呈现的所有单引号替换为双引号
最终代码和成果
最终代码如下:
function getType(o) {
return typeof o === "symbol"
? "Symbol_basic"
: Object.prototype.toString.call(o).slice(8, -1);
}
function isObject(o) {return o !== null && (typeof o === "object" || typeof o === "function");
}
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 null;
}
}
function checkCircular(obj, currentParent) {let type = getType(obj);
if (type == "Object" || type == "Array") {if (currentParent.includes(obj)) {throw new TypeError("Converting circular structure to JSON");
}
currentParent.push(obj);
}
}
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,'"');
} else {res = [];
Object.keys(target).forEach((key) => {if (getType(key) !== "Symbol_basic") {let type = getType(target[key]);
if (!specialList.includes(type)) {let currentParent = [...initParent];
checkCircular(target[key], currentParent);
res.push(`"${key}":${jsonStringify(target[key], currentParent)}`);
}
}
});
res = `{${res}}`.replace(/'/g,'"');
}
}
return res;
}
}
拿上面的 obj
对象测试一下成果:
let obj = {tag: Symbol("student"),
money: undefined,
girlfriend: null,
fn: function(){},
info1: [1,'str',NaN,Infinity,-Infinity,undefined,null,() => {},Symbol()],
info2: [new Set(),new Map(),new Error(),/a+b/],
info2: {
name: 'Chor',
age: 20,
male: true
},
info3: {date: new Date(),
tag: Symbol(),
fn: function(){},
un: undefined
},
info4:{str: new String('abc'),
no: new Number(123),
bool: new Boolean(false),
tag: Object(Symbol())
}
}
后果如下:
阐明咱们的实现是没有问题的。最初,我并没有实现 JSON.stringify()
中的 replacer 参数和 space 参数,感兴趣的读者能够在下面代码的根底上进一步拓展。
本文到此结束,感激你的浏览。若发现文中有谬误之处,欢送评论区指出。