wuxianqiang / blog

博客系列
17 stars 4 forks source link

JSON.stringify()实现原理 #285

Open wuxianqiang opened 4 years ago

wuxianqiang commented 4 years ago

JSON 的语法可以表示以下三种类型的值。

1、 简单值:使用与JavaScript 相同的语法,可以在JSON 中表示字符串、数值、布尔值和null。但JSON 不支持JavaScript 中的特殊值undefined。

2、 对象:对象作为一种复杂数据类型,表示的是一组无序的键值对儿。而每个键值对儿中的值可以是简单值,也可以是复杂数据类型的值。

3、 数组:数组也是一种复杂数据类型,表示一组有序的值的列表,可以通过数值索引来访问其中的值。数组的值也可以是任意类型——简单值、对象或数组。

JSON 不支持变量、函数或对象实例,它就是一种表示结构化数据的格式,虽然与JavaScript 中表示 数据的某些语法相同,但它并不局限于JavaScript 的范畴。

let myObj = {
  undef: undefined,
  bool: false,
  fun: function(){},
  date: new Date(),
  arr: [1, 2],
  obj: {a: 1, b: 2},
  reg: /\d/,
  sym: Symbol(),
  nul: null,
  set: new Set(),
  map: new Map()
}
console.log(JSON.stringify(myObj))
// {"bool":false,"date":"2020-05-27T03:22:47.587Z","arr":[1,2],"obj":{"a":1,"b":2},"reg":{},"nul":null,"set":{},"map":{}}

这个例子使用 JSON.stringify() 把一个JavaScript 对象序列化为一个JSON 字符串,

现在,我们已经了解了 JSON.stringify() 方法的输出以及它的工作方式,让我们从序列化值开始实现它。

序列化值,首先,我们将从以下数据类型开始。

1、undefined

2、number

3、boolean

4、string

function stringify(value) {
  // 参数类型
  var type = typeof value;

  function getValues(value) {
    if (type === "undefined") {
      return undefined;
    }

    if (type === "number" || type === "boolean") {
      return "" + value + "";
    }

    if (type === "string") {
      return '"' + value + '"';
    }
  }

  return getValues(value);
}

console.log(stringify(1)); // "1"

console.log(stringify("abc")); // ""abc""

console.log(stringify(true)); // "true"

// 这里是 undefined 而不是 "undefined"
console.log(stringify(undefined) === JSON.stringify(undefined)); // true

到目前为止,上述功能是比较简单。它所做的只是用引号把值引起来,但是undefined并不需要转换为字符串,而是直接返回undefined数据类型。

现在,我们将添加对更多数据类型的支持,例如

1、array

2、object

3、null

4、date

5、functions (methods)

为了支持数组和对象,我们应该解析属性之间的多层嵌套。我们必须递归地处理子元素并序列化值。

对于数组而言,它非常简单,请使用一个开括号和一个闭括号对数组进行迭代,然后调用该stringify()函数,然后依次调用该getValues()函数并重复进行,直到所有值都考虑在内。

但是对于对象,我们需要使用对象字面量的左,右括号将值和属性的序列化。

对于日期对象,还有一件有趣的事情,JSON.stringify()方法返回的值为 ISO 8601 日期字符串(与在Date对象上调用toISOString()的结果完全一样)。

function stringify(value) {
  var type = typeof value;

  function getValues(value) {
    if (type === "undefined" || type === "function") {
      return undefined;
    }

    if (type === "number" || type === "boolean") {
      return "" + value + "";
    }

    if (type === "string") {
      return '"' + value + '"';
    }
  }

  // 对于对象数据类型
  // 在javascript中,数组和对象都是对象
  if (type === "object") {
    // 检查值是否为null
    if (!value) {
      return "" + value + "";
    }

    // 检查值是否为日期对象
    if (value instanceof Date) {
      return '"' + new Date(value).toISOString() + '"'; // 返回ISO 8601日期字符串
    }

    // 检查值是否为Array
    if (value instanceof Array) {
      return "[" + value.map(stringify) + "]"; // 递归调用stringify函数

    } else {
      // 否则它只是一个对象
      // 递归调用stringify函数
      return (
        "{" +
        Object.keys(value).map(
          key => '"' + key + '"' + ":" + stringify(value[key])
        ) +
        "}"
      );
    }
  }

  return getValues(value);
}

console.log(stringify([1, 2, 3])); // "[1,2,3]"

console.log(stringify(new Date())); // prints date in ISO format

console.log(stringify({ a: 1 })); // ""{a:1}""

console.log(stringify(myObj) === JSON.stringify(myObj)); // true

console.log(JSON.parse(stringify(myObj))); // 类似 JSON.parse(JSON.stringify(myObj))

上面的函数现在适用于所有数据类型,并且输出与 JSON.stringify() 方法相同。

实际上,JSON.stringify() 除了要序列化的JavaScript 对象外,还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化JavaScript 对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在JSON 字符串中保留缩进。单独或组合使用这两个参数,可以更全面深入地控制JSON 的序列化。(这里实现的代码暂时不支持者两个参数)

与JavaScript 不同,JSON 中对象的属性名任何时候都必须加双引号。手工编写JSON 时,忘了给对象属性名加双引号或者把双引号写成单引号都是常见的错误。

如果对象中有 undefined,那么相应的属性会被忽略。

现在你已经了解了JSON在格式化数据的时候转换过程是怎样的,因为JSON对有些数据类型是不支持的,所以不建议大家使用JSON进行对象的深度克隆

JSON.parse(JSON.stringify(obj))

如果你需要用到深度克隆,可以参考下面的写法:

function deepClone (obj, hash = new WeakMap()) {
  if (obj == null) return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (typeof obj === 'symbol') {
    let desc = obj.description
    return desc ? Symbol(desc) : Symbol()
  }
  if (typeof obj !== 'object') return obj;
  if (hash.has(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor;
  hash.set(obj, cloneObj);
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = obj[key];
    }
  }
  return cloneObj;
}

上面代码使用了WeakMap数据结构来解决循环引用的问题,使用JSON拷贝循环引用的对象是会报错的,我们先了解一下什么是循环引用,也就是对象的某个属性引用对象本身。

let obj = { a: 1 }
obj.b = obj

由于WeakMap只接受对象作为键名,我们可把拷贝之前的对象作为键名,拷贝之后的对象作为键值,调用WeakMap的get方法读取对象键名,如果存在说明这个对象发生了循环引用,然后直接返回键值就是拷贝之后的对象,而不用再次递归。

关于WeakMap知识你得先了解其他几种数据结构:

Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。

Map

JavaScript 的对象(Object),本质上是键值对的集合,但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,对象 Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应。

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有两点。

首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。