JSON.stringify 模拟实现

805 阅读4分钟

JSON.stringify 模拟实现

JSON.stringify是我们开发过程中最常用的API之一,它的作用是将一个 JavaScript 对象或值转换为 JSON 字符串。

看一下它的方法签名:JSON.stringify(value[, replacer [, space]])

第一个参数是要转化为JSON字符串的JavaScript对象,第二个参数是用来替换指定值的函数,第三个参数一般用来做格式化。

此次我们实现仅有第一个参数的情况。

拿出 MDN 的说明:

  • 异常情况

    • 当在循环引用时会抛出异常 TypeError ("cyclic object value")(循环对象值)
    • 当尝试去转换 BigInt 类型的值会抛出 TypeError ("BigInt value can't be serialized in JSON")(BigInt 值不能 JSON 序列化).
  • 转换处理

    • 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
    • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
    • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
    • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如 JSON.stringify(function(){}) or JSON.stringify(undefined).
    • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
    • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
    • Date 日期调用了 toJSON() 将其转换为了 string 字符串(同 Date.toISOString()),因此会被当做字符串处理。
    • NaN 和 Infinity 格式的数值及 null 都会被当做 null。

分析

如上情况分析几个细节点: 1、对象中含有循环引用,那如何去判断中对象中是否含有循环引用呢?常规的做法就是将所有对象中放到一个 WeakMap 中,每次遍历对象属性时判断一下该对象是不是在已经被存放到 WeakMap 中,如果已经存放过了,则可以断定该对象内部含有循环引用,如下一段代码说明一下情况。(没有考虑辩解情况,仅用来示例)

const a = {}
a.a = a

const isValidObject = obj => typeof obj === "object" && obj !== null
function cycle(obj, map = new WeakMap()) {
  if (isValidObject(obj) && map.has(obj)) {
    throw Error("cyclic object value")
  }
  isValidObject(obj) && map.set(obj, true)
  for (let key in obj) {
    cycle(obj[key], map)
  }
}

cycle(a)

这个地方为什么会WeakMap么,我们知道js对象回收的时候会使用标记算法来对循环引用对象进行标记回收,而WeakMap中的引用关系是弱引用的,当GC执行标记操作时,不会因为被WeakMap引用就认为是活动对象,那么如果没有被其他对象所引用的话,在下一次GC执行时,内存就会被释放掉,避免内存的浪费。

2、当尝试去转换 BigInt 类型的值会抛出 TypeError ("BigInt value can't be serialized in JSON")BigInt 值不能JSON 序列化).

那么如何去判断一个对象是 BigInt 呢,可以使用类型替换的函数 Object.prototype.toString.call(obj) 去判断值是否为 [object BigInt]

3、NaNInfinity的判断

可以使用Number.isNaN来判断是否是NaN,Infinity则可以直接用相等来判断。

4、对象判断,如果一个对象含有toJSON属性,并且该属性类型是function,则会获取该function的返回值。

代码

其他地方没有什么难以理解的,代码和测试如下:

const isObject = obj => typeof obj === "object" && obj !== null

function jsonStringify(obj, map = new WeakMap()) {
  // 先判断循环引用
  if (isObject(obj) && map.has(obj)) throw TypeError("cyclic object value")
  isObject(obj) && map.set(obj, true)
  // 判断BigInt
  if (Object.prototype.toString.call(obj) === "[object BigInt]") {
    throw TypeError("Do not know how to serialize a BigInt")
  }
  if (typeof obj === "string") {
    return `"${obj}"`
  }
  if (obj === Infinity || Number.isNaN(obj) || obj === null) {
    return "null"
  }
  if (
    obj === undefined ||
    typeof obj === "function" ||
    typeof obj === "symbol"
  ) {
    return undefined
  }
  if (isObject(obj)) {
    if (obj.toJSON && typeof obj.toJSON === "function") {
      return jsonStringify(obj.toJSON(), map)
    } else if (Array.isArray(obj)) {
      let result = []
      obj.forEach((item, index) => {
        let value = jsonStringify(item, map)
        if (value !== undefined) {
          result.push(value)
        }
      })
      return `[${result}]`
    } else {
      let result = []
      Object.keys(obj).forEach(item => {
        let value = jsonStringify(obj[item], map)
        if (value !== undefined) {
          result.push(`"${item}":${value}`)
        }
      })
      return ("{" + result + "}").replace(/'/g, '"')
    }
  }
  return String(obj)
}

let nl = null
console.log(jsonStringify(nl) === JSON.stringify(nl))
// true

let und = undefined
console.log(jsonStringify(undefined) === JSON.stringify(undefined))
// true

let boo = false
console.log(jsonStringify(boo) === JSON.stringify(boo))
// true

let nan = NaN
console.log(jsonStringify(nan) === JSON.stringify(nan))
// true

let inf = Infinity
console.log(jsonStringify(Infinity) === JSON.stringify(Infinity))
// true

let str = "jack"
console.log(jsonStringify(str) === JSON.stringify(str))
// true

let reg = new RegExp("w")
console.log(jsonStringify(reg) === JSON.stringify(reg))
// true

let date = new Date()
console.log(jsonStringify(date) === JSON.stringify(date))
// true

let sym = Symbol(1)
console.log(jsonStringify(sym) === JSON.stringify(sym))
// true

let array = [1, 2, 3]
console.log(jsonStringify(array) === JSON.stringify(array))
// true

let obj = {
  name: "jack",
  age: 18,
  attr: ["coding", 123],
  date: new Date(0),
  uni: Symbol(2),
  sayHi: function () {
    console.log("hi")
  },
  info: {
    sister: "lily",
    age: 16,
    intro: {
      money: undefined,
      job: null,
    },
  },
}

console.log(jsonStringify(obj) === JSON.stringify(obj))
// true