Skip to content
大纲

实现 JSON.stringify 与 JSON.parse

定义

MDN


JSON 对象包含两个方法: 用于解析 JavaScript Object Notation (JSON) 的 parse() 方法,以及将对象/值转换为 JSON 字符串的 stringify() 方法。除了这两个方法, JSON 这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。


JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null 。它基于 JavaScript 语法,但与之不同:JavaScript 不是 JSON,JSON 也不是 JavaScript

参考JSON:并不是 JavaScript 的子集

JavaScript 与 JSON 的差异

JSON 是用于序列化对象、数组、数字、字符串、布尔值和 null 的语法。它基于 JavaScript 语法,但与 JavaScript 不同:大多数 JavaScript 都不是 JSON。例如:

  • Objects and Arrays 对象和数组。
    • 属性名称必须是双引号字符串;禁止尾随逗号。
  • 数字
    • 禁止使用前导零。
    • 小数点后必须至少跟一位数字。
    • 不支持 NaN 和 Infinity 。

任何 JSON 文本都是有效的 JavaScript 表达式,但仅在 JSON 超集修订之后。在修订之前,允许在 JSON 中的字符串文本和属性键中使用 U+2028 行分隔符和 U+2029 段落分隔符;但在 JavaScript 字符串文字中相同的用法是 SyntaxError 。

其他差异包括只允许双引号字符串,不支持 undefined 或注释。对于那些希望使用基于 JSON 的更人性化的配置格式的人来说,有 Babel 编译器使用的 JSON5 和更常用的 YAML。

JSON.stringify(value[, replacer [, space]])

JSON.stringify 是浏览器高版本带的一个将 JS 的 Object 对象转换为JSON 字符串的一个方法

该方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。

  • 异常
    • 当在循环引用时会抛出异常 TypeError ("cyclic object value")(循环对象值)obj['a'] = obj
    • 当尝试去转换 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()),因此会被当做字符串处理(格式如 2022-02-17T05:46:30.441Z)。
    • NaNInfinity, -Infinity 格式的数值及 null 都会被当做 null
    • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性(不可枚举的属性默认会被忽略)。

简单记忆

  • 会处理的几种类型: String, Number, Boolean, null, Array, Object
  • 不会处理的几种类型: Date, RegExp, Symbol, undefined, Function
js
const noop = () => {}
const obj = {
  null: null,
  undefined: undefined, // 忽略, 在数组中会转为 null
  symbol: Symbol(), // 忽略, 在数组中会转为 null
  fn_1: noop, // 忽略, 在数组中会转为 null
  nan: NaN, // 转为 null
  regexp: /abc/, // 转为 '{}' 字符串
  num_1: 0,
  num_2: -0, // 转为 0
  num_3: Infinity, // 转为 null
  num_4: -Infinity, // 转为 null
  date: new Date('2022-02-17T08:53:59.659Z'), // 转为字符串 2022-02-17T08:53:59.659Z
  bool_1: true,
  bool_2: false,
  // 值在数组中
  arr_1: [NaN, 0, -0, Infinity, new Date('2022-02-17T08:53:59.659Z')],
  arr_2: [undefined, Symbol(), noop] // 作为属性会忽略,在数组中会转为 null
}

// key 为 Symbol 也会忽略

console.log(JSON.stringify(noop)) // 函数       单独转换时,会返回 undefined
console.log(JSON.stringify(undefined)) // undefined 单独转换时,会返回 undefined
console.log(JSON.stringify(obj, null, 2))

参见手写实现 JSON.stringify,加深理解

手写实现 JSON.stringify

js
// 实现 JSON.stringify
// 参见 https://github.com/YvetteLau/Step-By-Step/issues/39#issuecomment-508327280

const jsonStringify = function jsonStringify(data) {
  let dataType = typeof data

  if (dataType !== 'object') {
    let result = data
    // data 可能是 string/number/null/undefined/boolean
    if (Number.isNaN(data) || data === Infinity) {
      // NaN 和 Infinity 序列化返回 'null'
      result = 'null'
    } else if (
      dataType === 'function' ||
      dataType === 'undefined' ||
      dataType === 'symbol'
    ) {
      // function, undefined, symbol 序列化返回 undefined
      return undefined
    } else if (dataType === 'string') {
      result = `"${data}"`
    }
    // boolean 返回 String()
    return String(result)
  } else if (dataType === 'object') {
    if (data === null) {
      return 'null'
    } else if (data.toJSON && typeof data.toJSON === 'function') {
      return jsonStringify(data.toJSON())
    } else if (data instanceof Array) {
      let result = []
      // 如果是数组
      // toJSON 方法可以存在于原型链中
      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 {
      // 普通对象
      /**
       * 循环引用抛错(暂未检测,循环引用时,堆栈溢出)
       * symbol key 忽略
       * undefined, 函数, symbol 为属性值,被忽略
       */
      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、函数、symbol 为属性值,忽略
            result.push(`"${item}":${jsonStringify(data[item])}`)
          }
        }
      })
      return `{${result}}`.replace(/'/g, `"`)
    }
  }
}

JSON.parse(text[, reviver])

该方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换(操作)。

如果指定了 reviver 函数,则解析出的 JavaScript 值(解析值)会经过一次转换后才将被最终返回(返回值)。

  • 异常
    • 若传入的字符串不符合 JSON 规范,则会抛出 SyntaxError 异常。
    • JSON.parse() 不允许用逗号作为结尾
js
const jsonStr = '{}'

console.log(JSON.parse(jsonStr))

手写实现 JSON.parse

  • 使用 eval 实现
  • 使用 new Function 实现

手写实现 JSON.parse

js
// 实现 JSON.parse

// 1. 使用 eval 实现
function parse1(json) {
  return eval(`(${json})`)
}

// 2. 上述方案如果数据中传入了可执行的 JS 代码,很可能造成 XSS 攻击,
// 因此调用 eval 之前,需要对数据进行校验
function parse2(json) {
  const rx_one = /^[\],:{}\s]*$/
  const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g
  const rx_three =
    /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g
  const rx_four = /(?:^|:|,)(?:\s*\[)+/g

  if (
    rx_one.test(
      json.replace(rx_two, '@').replace(rx_three, ']').replace(rx_four, '')
    )
  ) {
    return eval(`(${json})`)
  }
}

// 3. 使用 new Function 实现
// Function 与 eval 有相同的字符串参数特性
function parse3(json) {
  return new Function(`return ${json}`)()
}

// 以下是 MDN polyfill 的实现
// From https://github.com/douglascrockford/JSON-js/blob/master/json2.js
if (typeof JSON.parse !== 'function') {
  var rx_one = /^[\],:{}\s]*$/
  var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g
  var rx_three =
    /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g
  var rx_four = /(?:^|:|,)(?:\s*\[)+/g
  var rx_dangerous =
    /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g
  JSON.parse = function (text, reviver) {
    // The parse method takes a text and an optional reviver function, and returns
    // a JavaScript value if the text is a valid JSON text.

    var j

    function walk(holder, key) {
      // The walk method is used to recursively walk the resulting structure so
      // that modifications can be made.

      var k
      var v
      var value = holder[key]
      if (value && typeof value === 'object') {
        for (k in value) {
          if (Object.prototype.hasOwnProperty.call(value, k)) {
            v = walk(value, k)
            if (v !== undefined) {
              value[k] = v
            } else {
              delete value[k]
            }
          }
        }
      }
      return reviver.call(holder, key, value)
    }

    // Parsing happens in four stages. In the first stage, we replace certain
    // Unicode characters with escape sequences. JavaScript handles many characters
    // incorrectly, either silently deleting them, or treating them as line endings.

    text = String(text)
    rx_dangerous.lastIndex = 0
    if (rx_dangerous.test(text)) {
      text = text.replace(rx_dangerous, function (a) {
        return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
      })
    }

    // In the second stage, we run the text against regular expressions that look
    // for non-JSON patterns. We are especially concerned with "()" and "new"
    // because they can cause invocation, and "=" because it can cause mutation.
    // But just to be safe, we want to reject all unexpected forms.

    // We split the second stage into 4 regexp operations in order to work around
    // crippling inefficiencies in IE's and Safari's regexp engines. First we
    // replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
    // replace all simple value tokens with "]" characters. Third, we delete all
    // open brackets that follow a colon or comma or that begin the text. Finally,
    // we look to see that the remaining characters are only whitespace or "]" or
    // "," or ":" or "{" or "}". If that is so, then the text is safe for eval.

    if (
      rx_one.test(
        text
          .replace(rx_two, '@')
          .replace(rx_three, ']')
          .replace(rx_four, '')
      )
    ) {
      // In the third stage we use the eval function to compile the text into a
      // JavaScript structure. The "{" operator is subject to a syntactic ambiguity
      // in JavaScript: it can begin a block or an object literal. We wrap the text
      // in parens to eliminate the ambiguity.

      j = eval('(' + text + ')')

      // In the optional fourth stage, we recursively walk the new structure, passing
      // each name/value pair to a reviver function for possible transformation.

      return typeof reviver === 'function'
        ? walk(
            {
              '': j
            },
            ''
          )
        : j
    }

    // If the text is not JSON parseable, then a SyntaxError is thrown.

    throw new SyntaxError('JSON.parse')
  }
}

扩展

资料

参考