JSON.stringify() 优化

115 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情

  • 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 参数。