这是JS 原生办法原理探索系列的第八篇文章。本文会介绍如何实现 JSON.stringify() 办法。

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 用于寄存比拟非凡的 UndefinedSymbol_basicFunction 三种类型,非凡在于:对象 key 的 value 如果是这些类型,则序列化的时候会失落,数组的元素如果是这些类型,则序列化的时候会对立转化为 "null"。因为这三种类型要屡次用到,所以先存起来。
  • 为什么要将最终返回的 res 初始化为一个空数组?因为:

    • 如果咱们解决的 target 是数组,则只须要调用 map 就能够将数组的每一个元素映射为序列化之后的后果,调用后返回的数组赋给 res,再和 [] 字符拼接,会隐式调用数组的 toString 办法,产生一个规范的序列化后果;
    • 如果解决的 target 是对象字面量,则能够将它的每个 key-value 的序列化后果 push 到 res 中,最终再和 {} 字符拼接,也同样会产生一个规范的序列化后果。
    • 在整个过程中不须要去解决 JSON 字符串中的逗号分隔符。
  • 对于对象字面量,类型为 "Symbol_basic" 的属性会失落,属性值为 UndefinedSymbol_basicFunction 三种类型的属性也会失落。属性失落其实就是在遍历对象的时候略过这些属性
  • 在检测循环援用的时候,存在嵌套关系的对象应该共享同一条父级链,所以递归的时候须要把寄存父级链的数组传进去;同时,不存在嵌套关系的两个对象不应该共享同一条父级链(否则会将所有相互援用的状况都误认为是循环援用),所以每次遍历对象 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 参数,感兴趣的读者能够在下面代码的根底上进一步拓展。

本文到此结束,感激你的浏览。若发现文中有谬误之处,欢送评论区指出。