如何实现 JSON.stringify()?

更新日期: 2021-07-09阅读: 1.4k标签: json

JSON.stringify() 可以将对象或值转化为 JSON 字符串。理论上,它可以接受很多种不同的数据类型作为参数,而不同的数据类型,处理和转化的结果也不同。所以在实现这个方法之前,我们先弄清楚具体的处理规则。

不同数据类型的处理结果

先看基本数据类型:

数据类型处理结果数据类型处理结果
String返回'"string"'Number返回 "1234"(NaN,±Infinity 返回 "null")
Null返回“null”Undefined返回 undefined
Symbol返回 undefinedBoolean返回 "true"/"false"

再看引用数据类型:

数据类型处理结果数据类型处理结果
对象字面量递归序列化。但是值为 undefined / Symbol / 函数类型的属性、类型为 Symbol 的属性会丢失类数组对象同对象字面量
基本类型的包装对象一般返回包装对象的 valueOf(string 类型前后要加引号)的字符串形式,但是 Symbol 类型返回 "{}"数组递归序列化。但是 undefined、Symbol、函数类型的属性会返回 "null"
Map返回 "{}"Set返回 "{}"
Error返回 "{}"RegExp返回 "{}"
Function返回 undefinedDate返回调用 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 参数,感兴趣的读者可以在上面代码的基础上进一步拓展。

来自:https://segmentfault.com/a/1190000040321449


链接: https://www.fly63.com/article/detial/10499

web数据格式中关于:XML/HTML/JSON学习总汇

这篇文章讲解关于XML/HTML/JSON的学习,大家都知道服务器端可以返回的数据格式,主要就是:XML、HTML、JSON,当我们做数据抓取,ajax请求的时候都需要熟悉它们的使用。

解决IE8下JSON.stringify()自动将中文转译成unicode的方法

在IE8下JSON.stringify()自动将中文转译为unicode编码,原本选择的中文字符,传到后台变为了unicode编码,即u****的形式。查找资料后发现,与标准的JSON.stringify()不同,IE8内置的JSON.stringify()会自动将编码从utf-8转为unicode编码,导致出现这种类似于乱码的情况。

js实现json格式化,以及json校验工具的简单实现

这篇文章主要讲解:json结构及形式、json字符串转化为json对象【通过eval( ) 方法,new Function形式,使用全局的JSON对象】、json校验格式化工具简单实现

解析Json字符串的三种方法

在很多时候,我们的需要将类似 json 格式的字符串数据转为json,下面将介绍日常中使用的三种解析json字符串的方法

解决IE8以下低版本实现JSON.parse()与JSON.stringify()的兼容

将字符串和json对象的相互转换,我们通常使用JSON.parse()与JSON.stringify()。解决IE8以下低版本实现JSON.parse()与JSON.stringify()的兼容呢:利用eval方式解析、new Function形式、自定义兼容json的方法、head头添加mate等

什么是数据交互格式?xml和json优缺点

就是客户端和服务端进行信息传输的格式(xml和json),双方约定用什么格式进行传输,然后解析得到自己想要的值,xml扩展标记语言,属于重量级(第一占宽带、第二解析难),json属于轻量级的数据交互格式(不占宽带,解析很简单)

js 将json字符串转换为json对象的方法解析

将json字符串转换为json对象的方法。在数据传输过程中,json是以文本,即字符串的形式传递的,而JS操作的是JSON对象,所以,JSON对象和JSON字符串之间的相互转换是关键

聊聊JSON Schema

json现在已经成为比较通用灵活的数据交换格式,尤其是在web方面,总是少不了它的身影,js原生就支持它。网页中与服务器中和服务器交换信息也基本上式基于json的。在现在的开发中,特别是在前后端分离的开发中,后端提供接口,前端通过接口拿取数据;

百度JSON LD结构化数据代码分享

百度JSON LD结构化数据代码分享,搞外贸网站,企业网站这么就,对谷歌的 schema 结构化数据比较熟悉,但是对百度的结构化数据就了解太少了

什么是JWT(JSON WEB TOKEN)

Json web token(JWT)是为了网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC 7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!