当前位置: 首页 > Web前端 > JavaScript

JS原生方法原理探究(八):如何实现JSON.stringify()?

时间:2023-03-27 15:29:39 JavaScript

这是探索JS原生方法原理系列文章的第八篇。本文介绍如何实现JSON.stringify()方法。JSON.stringify()可以将对象或值转换为JSON字符串。理论上,它可以接受多种不同的数据类型作为参数,不同的数据类型有不同的处理和转换结果。所以在实现这个方法之前,我们先弄清楚具体的处理规则。不同数据类型的处理结果先看基本数据类型:数据类型处理结果数据类型处理结果String返回'"string"'Number返回"1234"(NaN,±Infinity返回"null")Null返回"null"undefined返回undefinedSymbol返回undefinedBoolean返回“true”/“false”看引用数据类型:数据类型处理结果数据类型处理结果对象字面量递归序列化。但是,值为undefined/Symbol/function类型的属性,以及类型为Symbol的属性将失去类数组对象和对象字面量基本类型的包装对象。一般情况下,wrapper对象的valueOf(string类型前后带引号)作为字符串返回。但是Symbol类型返回递归序列化的“{}”数组。但是undefined、Symbol、function类型的属性会返回"null"Map返回"{}"Set返回"{}"错误返回"{}"RegExp返回"{}"函数返回undefinedDate返回调用JSON后生成的字符串实现在下面的代码实现中,将思想分为基本数据类型和引用数据类型两种:基本数据类型:返回按照上述规则序列化后的结果。重点处理未定义类型、符号类型、数字类型中的NaN、±Infinity。引用数据类型(根据是否可以继续遍历分为两种):可以继续遍历的类型:包括对象字面量、数组、类数组对象、Set、Map。遍历时可以跳过需要丢失的属性。不能进一步遍历的类型:包括基本类型的包装对象、Error对象、常规对象、日期对象函数。一个函数集中处理此外,在遍历数组或对象时,还需要检测是否存在循环引用。如果有,需要抛出对应的错误数据类型,使用getType判断具体的数据类型。因为基本类型Symbol和它的包装类型的处理方式不同,所以基本类型Symbol使用“Symbol_basic”,它的包装类型使用“Symbol”。函数getType(o){返回类型o===“符号”?"Symbol_basic":Object.prototype.toString.call(o).slice(8,-1);}使用isObject判断是引用类型还是基本类型:functionisObject(o){returno!==null&&(typeofo==='object'||:functionprocessOtherTypes(target,type){switch(type){case'String':return`"${target.valueOf()}"`case'Number':case'Boolean':returntarget.valueOf().toString()case'Symbol':case'Error':case'RegExp':return"{}"case'Date':return`"${target.toJSON()}"`case'Function':returnundefineddefault:return""}}特别注意Stringwrapper类型,不能直接返回其valueOf(),必须在其两边加上引号。比如{a:"bbb"},我们期望的序列化结果应该是'{a:"bbb"}'而不是'{a:bbb}';同样,对于Date对象,直接返回它的toJSON()会得到'{date:1995-12-16T19:24:00.000Z}',而我们要的是'{date:"1995-12-16T19:24:00.000Z"}',所以前后还要加上引号。检测循环引用循环引用是指对象的结构是循环的,不是树状的://下面的对象/数组有循环引用letobj={};obj.a=obj;letobj1={a:{b:{}}};obj1.a.b.c=obj1.a;letarr=[1,2];arr[2]=arr;//注意这个对象没有循环引用,只有平面引用letobj2={a:{}};obj2.b=obj2.a;如何检测循环引用?考虑最简单的情况,只有当key对应的值是对象或数组时,才可能存在循环引用。因此,在遍历key时,循环引用只有在确定value是对象或数组后才会处理。每个key都会有自己的数组来存放父链,递归的时候总是传递这个数组。如果检测到当前key对应的值已经出现在数组中,证明引用了父对象,可以抛出错误;如果没有出现,则添加到数组中,更新父链,使其成为一般的循环引用检测函数如下:functioncheckCircular(target,parentArray=[target]){Object.keys(target).forEach(key=>{if(typeoftarget[key]=='object'){if(parentArray.inlcudes(target[key])||checkCircular(target[key],[target[key],...parentArray])){thrownewError('存在循环引用')}}})console.log('不存在循环引用')}在JSON.stringify的实现中,遍历key的过程主代码中已经完成,所以这里的checkCircular只需要包含检测过程即可。稍微修改如下:')}currentParent.push(target)}核心代码最终实现的核心代码如下:functionjsonStringify(target,initParent=[target]){lettype=getType(target)letiterableList=['Object','Array','Arguments','Set','Map']letspecialList=['Undefined','Symbol_basic','Function']//如果是基本数据类型if(!isObject(target)){if(type==='Symbol_basic'||type==='Undefined'){returnundefined}elseif(Number.isNaN(target)||target===Infinity||target===-Infinity){return"null"}elseif(type==='String'){return`"${target}"`}returnString(target)}//如果是引用数据类型else{letres//如果是类型无法遍历if(!iterableList.includes(type)){res=processOtherTypes(target,type)}//如果是可遍历类型else{//如果是数组if(type==='Array'){res=target.map(item=>{if(specialList.includes(getType(item))){return"null"}else{//检查循环引用letcurrentParent=[...initParent]checkCircular(item,currentParent)returnjsonStringify(item,currentParent)}})res=`[${res}]`.replace(/'/g,'"')}//Ifobjectliteral,array-likeobject,Set,Mapelse{res=[]Object.keys(target).forEach(key=>{//符号类型key直接跳过if(getType(key)!=='Symbol_basic'){letkeyType=getType(target[key])if(!specialList.includes(keyType)){//检查循环引用letcurrentParent=[...initParent]checkCircular(target[key],currentParent)//将键值对压入数组res.push(`"${key}":${jsonStringify(target[key],currentParent)}`)}}})res=`{${res}}`.replace(/'/g,'"')}}returnres}}基本按照上面的table有几个细节可以注意一下:iterableList用来存放可以继续遍历的数据类型;specialList用来存放Undefined、Symbol_basic、Function等特殊类型,特殊之处在于:取值objectkeyif如果是这几种类型,在序列化的时候会丢失。如果数组的元素是这几种类型,那么在序列化的时候会统一转为“null”。因为这三种类型会被多次使用,它们首先存储。为什么将最终返回的res初始化为一个空数组呢?因为:如果我们处理的目标是一个数组,我们只需要调用map将数组的每个元素映射到序列化后的结果即可。调用数组的toString方法生成一个标准的序列化结果;如果要处理的目标是对象字面量,可以将每个key-value的序列化结果push到res中,最后用{,}字符拼接也会产生一个标准的序列化结果。全程无需处理JSON字符串中的逗号分隔符。对于对象字面量,“Symbol_basic”类型的属性会丢失,属性值为Undefined、Symbol_basic、Function的属性也会丢失。属性损失实际上是在遍历对象时跳过这些属性。在检测循环引用时,嵌套关系的对象应该共享同一个父链,所以在递归时,需要传入存放父链的数组;同时,没有嵌套关系的两个对象不应该共享同一个父链(否则所有的相互引用都会被误认为是循环引用),所以每次遍历对象key都会重新生成一个currentArray。最后,为了保险起见,记得把序列化结果中可能出现的单引号全部换成双引号最终代码及效果最终代码如下:functiongetType(o){returntypeofo===“象征”?"Symbol_basic":Object.prototype.toString.call(o).slice(8,-1);}functionisObject(o){returno!==null&&(typeofo==="object"||typeofo==="function");}functionprocessOtherTypes(target,type){switch(type){case"String":return`"${target.valueOf()}"`;case"Number":case"Boolean":返回目标。valueOf().toString();案例“符号”:案例“错误”:案例“正则表达式”:返回“{}”;case"Date":return`"${target.toJSON()}"`;案例“功能”:返回未定义;默认值:返回空值;}}functioncheckCircular(obj,currentParent){lettype=getType(obj);if(type=="Object"||type=="Array"){if(currentParent.includes(obj)){thrownewTypeError("ConvertingcircularstructuretoJSON");}currentParent.push(obj);}}函数jsonStringify(目标,initParent=[目标]){让类型=getType(目标);letiterableList=["Object","Array","Arguments","Set","Map"];letspecialList=["Undefined","Symbol_basic","Function"];if(!isObject(target)){if(type==="Symbol_basic"||type==="Undefined"){返回未定义;}elseif(Number.isNaN(target)||target===Infinity||target===-Infinity){return"null";}elseif(type==="String"){return`"${target}"`;}返回字符串(目标);}else{让res;如果(!iterableList.includes(type)){res=processOtherTypes(target,type);}else{if(type==="Array"){res=target.map((item)=>{if(specialList.includes(getType(item))){return"null";}else{letcurrentParent=[...initParent];checkCircular(item,currentParent);returnjsonStringify(item,currentParent);}});res=`[${res}]`.replace(/'/g,'"');}else{res=[];Object.keys(target).forEach((key)=>{if(getType(key)!=="Symbol_basic"){lettype=getType(target[key]);if(!specialList.includes(type)){letcurrentParent=[...initParent];checkCircular(target[key],currentParent);res.push(`"${key}":${jsonStringify(target[key],currentParent)}`);}}});res=`{${res}}`.replace(/'/g,'"');}}返回资源;}}拿下面的obj对图像测试一下结果:letobj={tag:Symbol("student"),money:undefined,girlfriend:null,fn:function(){},info1:[1,'str',NaN,Infinity,-Infinity,undefined,null,()=>{},Symbol()],info2:[newSet(),newMap(),newError(),/a+b/],info2:{name:'Chor',age:20,male:true},info3:{date:newDate(),标签:Symbol(),fn:function(){},un:undefined},info4:{str:newString('abc'),no:newNumber(123),bool:newBoolean(false),tag:Object(Symbol())}}结果如下:说明我们的实现没问题最后我没有在JSON.stringify()中实现replacer参数和space参数。有兴趣的读者可以在上述代码的基础上进一步扩展。本文到此结束,感谢阅读。如果大家发现文中有错误,欢迎在评论区指出。