相关文章推荐
微笑的松球  ·  批处理(bat)if ...·  1 年前    · 
讲道义的大海  ·  在Jupyter ...·  1 年前    · 

在我刚开始学习web开发时,JSON是看起来很简单的一个东西。因为JSON字符串看起来就像一个文本,JavaScript对象的的最小子集。在我职业生涯的早期,我从来没有花时间去好好研究这种数据格式。我仅仅只是使用 JSON.stringify JSON.parse ,直到出现意外的错误。

在这篇文章中,我想:

  • 总结一下我在JavaScript中使用JSON(更确切的说是 JSON.stringify API)时遇到的怪事
  • 通过从头开始实现JSON.stringify的简化版本,来加深我对JSON的理解
  • 什么是JSON

    JSON是 Douglas Crockford 发明的一种数据结构。你可能已经知道了这些。但是有意思的是,正如Crockford在他的书《JavaScript悟道》中写的那样,他承认:“关于JSON的最糟糕的事情就是名字。”

    JSON表示JavaScript对象表示法(JavaScript Object Notation)。问题在于,这个名字误导人们认为它只适用于JavaScript。然而事实上,它的目的是允许不同语言编写的程序有效地沟通。

    在类似的问题上,Crockford也坦言,JavaScript提供的两个内置API可以与JSON一起工作。它们是 JSON.parse JSON.stringify ,同样的,命名也很糟糕。它们应该分别被称为 JSON.decode JSON.encode ,因为 JSON.parse 需要一个JSON文本并将其 解码 为JavaScript值,而 JSON.stringify 需要一个JavaScript值并将其 编码 为JSON文本/字符串。

    说完了命名,让我们看看JSON支持哪些数据类型,以及当一个不兼容的JSON值被 JSON.stringify 字符串化时会发生什么。

    JSON支持哪些数据格式

    JSON有一个 官方网站 ,你可以在上面查看所有支持的数据类型,但是说实话,对于我来说,页面上的图有点难以理解。所以我更喜欢下面的类型注释:

    type Json = 
    	| null
    	| boolean
    	| number
    	| string
    	| Json[]
    	| {[key: string]: Json}
    

    对于任何不属于上述Json联合类型的数据类型,比如说undefinedSymbolBigInt ,以及其他内置对象,比如说FunctionMapSetRegex ,它们不被JSON支持,注释也一样不被支持。

    下一个合乎逻辑的问题是,在JavaScript的上下文中,当我们说一个数据类型不被JSON支持时,到底是什么意思?

    JSON.stringify的怪异行为

    在JavaScript中,通过JSON.stringify将值转换为JSON字符串。

    对于JSON支持的类型的值,它们会被转换为预期的字符串:

    JSON.stringify(1) // '1'
    JSON.stringify(null) // 'null'
    JSON.stringify('foo') // '"foo"'
    JSON.stringify({foo: 'bar'}) // '{"foo":"bar"}'
    JSON.stringify(['foo', 'bar']) // '["foo","bar"]'
    

    但在字符串化/编码过程中,如果涉及到不支持的类型,事情会变得棘手起来。

    当直接传递不支持的类型undefinedSymbol, 和 Function 时,JSON.stringify会输出undefined (不是'undefined' 字符串):

    JSON.stringify(undefined) // undefined
    JSON.stringify(Symbol('foo')) // undefined
    JSON.stringify(() => {}) // undefined
    

    对于其他内置对象类型(Function 和 Date 除外),比如说MapSetWeakMapWeakSetRegex 等等,JSON.stringify 会返回一个空对象字面量的字符串,也就是'{}'

    JSON.stringify(/foo/) // '{}'
    JSON.stringify(new Map()) // '{}'
    JSON.stringify(new Set()) //'{}'
    

    当被序列化的值位于数组或对象中时,会发生更多不一致的行为。

    对于不支持的导致undefined 的类型,也就是undefinedSymbolFunction ,当它们在数组中被发现时,会被转换为字符串'null' ;当它们在对象中被发现时,整个属性会从输出中省略:

    JSON.stringify([undefined]) // '[null]'
    JSON.stringify({foo: undefined}) // '{}'
    JSON.stringify([Symbol()]) // '[null]'
    JSON.stringify({foo: Symbol()}) // '{}'
    JSON.stringify([() => {}]) // '[null]'
    JSON.stringify({foo: () => {}}) // '{}'
    

    另一方面,对于其他内置对象类型,诸如MapSetRegex 等,存在于数组或对象中时,被JSON.stringify转换完毕后,都会变为空对象字面量的字符串,也就是'{}'

    JSON.stringify([/foo/]) // '[{}]'
    JSON.stringify({foo: /foo/}) // '{"foo":{}}'
    JSON.stringify([new Set()]) // '[{}]'
    JSON.stringify({foo: new Set()}) // '{"foo":{}}'
    JSON.stringify([new Map()]) // '[{}]'
    JSON.stringify({foo: new Map()}) // '{"foo":{}}'
    

    对于最近添加的新类型BigIntJSON.stringify 会抛出一个TypeError错误 。另一种情况时,当传递循环对象时,JSON.stringify会抛出错误。大多数情况下,JSON.stringify是相当宽容的。它不会因为你违反了JSON的规则而使你的程序崩溃(除非是BigInt或循环对象)。

    const foo = {}
    foo.a = foo
    JSON.stringify(foo) // ❌ Uncaught TypeError: Converting circular structure to JSON
    JSON.stringify(BigInt(1234567890)) // ❌ Uncaught TypeError: Do not know how to serialize a BigInt
    

    尽管是数字类型,NaNInfinity依然会被JSON.stringify转换为null。这个设计决定背后的原因是,正如Crockford在他的书《JavaScript悟道》中写到的,NaNInfinity的存在表明了一个错误。他通过使它们变成null来排除它们。

    JSON.stringify(NaN) // 'null'
    JSON.stringify(Infinity) // 'null'
    

    通过JSON.stringifyDate 对象会被编码为ISO字符串,因为具有Date.prototype.toJSON

    JSON.stringify(new Date()) // '"2022-06-01T14:22:51.307Z"'
    

    JSON.stringify只处理可枚举的、非符号键的对象属性。符号键、非枚举属性会被忽略:

    const foo = {}
    foo[Symbol('p1')] = 'bar'
    Object.defineProperty(foo, 'p2', {value: 'baz', enumerable: false})
    JSON.stringify(foo) // '{}'
    

    顺便说一下,希望你能明白为什么使用JSON.parseJSON.stringify来深克隆一个对象大多是一个坏主意。

    我知道要记住的东西很多,所以我整理了一份小抄,供你参考。

    自定义编码

    目前为止,我们所讨论的是,JavaScript如何通过JSON.stringify将值编码为JSON字符串的默认行为,有两种方式可以自行控制转换规则:

    添加一个toJSON方法,到你传递给JSON.stringify的对象上。这也是为什么Date对象传递给JSON.stringify不会导致一个空对象字面量。因为Date对象会从它的原型上继承toJSON方法。

    const foo = {
        toJSON: () => 'bar',
    JSON.stringify(foo) // 'bar'
    

    JSON.stringify接收一个称为replacer的可选参数,它可以是一个函数或一个数组,来改变字符串化过程的默认行为。

    简化版JSON.stringify

    下面是简化版JSON.stringify的实现。为了简洁起见,这里省略了可选参数replacer 和 space

    const isCyclic = (input) => {
      let seen = new Set()
      const dfs = (obj) => {
        if (typeof obj !== 'object' || obj === null) return false
        seen.add(obj)
        return Object.entries(obj).some(([key, value]) => {
          const result = seen.has(value) ? true : isCyclic(value)
          seen.delete(value)
          return result
      return dfs(input)
    function jsonStringify(data) {
      if (isCyclic(data))
        throw new TypeError('Converting circular structure to JSON')
      if (typeof data === 'bigint')
        throw new TypeError('Do not know how to serialize a BigInt')
      if (data === null) {
        // get rid of null first because the type of null is 'object'
        return 'null'
      const type = typeof data
      if (type !== 'object') {
        let result = data
        if (Number.isNaN(data) || data === Infinity) {
          // for NaN and Infinity we return 'null'
          result = 'null'
        } else if (
          type === 'function' ||
          type === 'undefined' ||
          type === 'symbol'
          return undefined
        } else if (type === 'string') {
          result = '"' + data + '"'
        return String(result)
      if (type === 'object') {
        if (typeof data.toJSON === 'function') {
          return jsonStringify(data.toJSON())
        if (data instanceof Array) {
          let result = []
          data.forEach((item, index) => {
              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) => {
            if (typeof item !== 'symbol') {
                data[item] !== undefined &&
                typeof data[item] !== 'function' &&
                typeof data[item] !== 'symbol'
                result.push('"' + item + '"' + ':' + jsonStringify(data[item]))
          return ('{' + result + '}').replace(/'/g, '"')