自己实现一个JSON.Stringify 方法

3,288 阅读4分钟

这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战

前言

JSON是日常开发中经常用到的一个对象,它包含两个方法:stringify用于将对象转换成JSON字符串,即序列化;parse()用于将JSON字符串转换成JS对象,即反序列化。这次我们来手动实现下stringify这个方法,通过实现的过程,我们能够站在全局考察自己对 JS 各种数据类型理解的深度,对各种极端的边界情况处理能力,以及编码能力。

方法介绍

stringify方法的作用是将一个JS对象或值转换成JSON格式的字符串,它有三个参数:

JSON.stringify(value[, replacer [, space]])
  • value:要进行序列化转换的一个JS对象或值。
  • replacer [Function | Array]:可选参数。如果是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者无,则对象所有的属性都会被序列化。
  • space [Number | String]:可选参数。指定缩进用的空白字符串,用于格式化转换的结果字符串;如果参数是个数字,则代表有缩进有多少个空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。

JSON/JS间数据类型转换

JSON的数据类型与JS非常相识,尤其是基础数据类型和只包含基础数据类型的数组,它们都能相互安全地进行转换,但是对于引用数据类型,要十分小心。

下图是数据转换格式表:

image.png

上面这个表中,基本列举了各种数据类型通过stringify转换后的值,但还有一个特殊情况需要注意:对于包含循环引用的对象执行此方法,会抛出错误。

image.png

实现

有了上面的分析,现在就有思路来自己实现一个了。首先就是判断传入的值是否是一个引用数据类型,然后根据上面分析的转换结果来进行处理:

  // 处理非引用数据类型
  if(type !== 'object') {
    let result = data;
    if (Number.isNaN(data) || data === Infinity) {
       //NaN 和 Infinity 序列化返回 "null"
       result = "null";
    } else if (type === 'function' || type === 'undefined' || type === 'symbol') {
      // 由于 function 序列化返回 undefined,因此和 undefined、symbol 一起处理
       return undefined;
    } else if (type === 'string') {
       result = '"' + data + '"';
    }
    return String(result);
  }

然后再处理引用数据类型,由于引用数据类型分内置引用对象和自定义引用对象,所以逻辑更加复杂:

  if (type === 'object') {
     // 判断null
     if (data === null) {
        return "null"
     } else if (data.toJSON && typeof data.toJSON === 'function') {
         // 对象可能内置toJSON方法来自定义序列化对象
        return jsonStringify(data.toJSON());
     } else if (data instanceof Array) {
        let result = [];
        //如果是数组,那么数组里面的每一项都需要判断
        data.forEach((item, index) => {
        if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
               result[index] = "null";
           } else {
               result[index] = jsonStringify(item);
           }
         });
         result = "[" + result + "]";
         return result.replace(/'/g, '"');
      } else {
         // 处理普通对象
         let result = [];
         Object.keys(data).forEach((item, index) => {
            if (typeof item !== 'symbol') {
              //key 如果是 symbol 对象,忽略
              if (data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') {
                //键值如果是 undefined、function、symbol 为属性值,忽略
                result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
              }
            }
         });
         return ("{" + result + "}").replace(/'/g, '"');
        }
   }

整体代码:

function jsonStringify(data) {
  let type = typeof data;
  if(type !== 'object') {
    let result = data;
    if (Number.isNaN(data) || data === Infinity) {
       //NaN 和 Infinity 序列化返回 "null"
       result = "null";
    } else if (type === 'function' || type === 'undefined' || type === 'symbol') {
      // 由于 function 序列化返回 undefined,因此和 undefined、symbol 一起处理
       return undefined;
    } else if (type === 'string') {
       result = '"' + data + '"';
    }
    return String(result);
  } else if (type === 'object') {
     if (data === null) {
        return "null"  
     } else if (data.toJSON && typeof data.toJSON === 'function') {
         // 对象可能内置toJSON方法来自定义序列化对象
        return jsonStringify(data.toJSON());
     } else if (data instanceof Array) {
        let result = [];
        data.forEach((item, index) => {
        if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
               result[index] = "null";
           } else {
               result[index] = jsonStringify(item);
           }
         });
         result = "[" + result + "]";
         return result.replace(/'/g, '"');
      } else {
         // 处理普通对象
         let result = [];
         Object.keys(data).forEach((item, index) => {
            if (typeof item !== 'symbol') {
              //key 如果是 symbol 对象,忽略
              if (data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') {
                //键值如果是 undefined、function、symbol 为属性值,忽略
                result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
              }
            }
         });
         return ("{" + result + "}").replace(/'/g, '"');
        }
    }
}

总结

从上面代码可以看到,整体逻辑还是十分复杂的,涉及到了多种数据类型,及转换边界,这里总结了几个要注意的地方:

  • 根据转换规则,function会被转换成null,除此之外,undefinedsymbol也会被转换为null,因此可以集中一起处理掉。
  • 使用typeof 判断 object时,要注意null也是object,因此要对null进行单独判断。
  • 对于数组中的元素,有可能是引用数据类型,因此可以对数组元素进行递归判断。