39. 创建和解析 JSON

JSON(“JavaScript Object Notation”)是一种使用文本对数据进行编码的存储格式。它的语法是 JavaScript 表达式的一个子集。例如,考虑以下数据,以文本形式存储在文件jane.json中:

{
  "first": "Jane",
  "last": "Porter",
  "married": true,
  "born": 1890,
  "friends": [ "Tarzan", "Cheeta" ]
}

JavaScript 具有全局命名空间对象JSON提供了用于创建和解析 JSON 的方法。

39.1。 JSON 的发现和标准化

道格拉斯克罗克福德于 2001 年在 json.org 上发表了 JSON 规范。他解释说:

我发现了 JSON。我并没有声称发明了 JSON,因为它已经存在于自然界中。我做的是我发现它,我命名它,我描述了它是如何有用的。我并不是第一个发现它的人;我知道还有其他人在我做之前至少一年才发现它。我发现的最早的事情是,早在 1996 年,Netscape 就有人使用 JavaScript 数组字面值进行数据通信,至少在我偶然发现这个想法之前的五年。

后来,JSON 被标准化为 ECMA-404

  • 第 1 版:2013 年 10 月
  • 第 2 版:2017 年 12 月

39.1.1. JSON 的语法被冻结了

引用 ECMA-404 标准:因为它非常简单,所以不期望 JSON 语法会发生变化。这使得 JSON 作为一种基本符号,具有极大的稳定性。

因此,JSON 永远不会得到诸如可选的尾随逗号,注释或不带引号的密钥之类的改进 - 无论它们是否被认为是合乎需要的。但是,这仍然留下了创建编译为普通 JSON 的 JSON 超集的空间。

39.2. JSON 语法

JSON 由以下 JavaScript 部分组成:

  • 复合:
    • 对象字面值:
      • 键是双引号字符串。
      • 值是 JSON 值。
      • 不允许使用尾随逗号。
    • 数组字面值:
      • 元素是 JSON 值。
      • 不允许使用漏洞或尾随逗号。
  • 原子:
    • null(但不是undefined
    • 布尔
    • 数字(不包括NaN+Infinity-Infinity
    • 字符串(必须双引号)

因此,您不能(直接)在 JSON 中表示循环结构。

39.3. 使用JSON API

全局命名空间对象JSON包含用于处理 JSON 数据的方法。

39.3.1. JSON.stringify(value, replacer?, space?)

.stringify()将 JavaScript value转换为 JSON 字符串。

39.3.1.1. 结果:单行文本
  • 如果只提供第一个参数,.stringify()将返回单行文本:
    assert.equal(
      JSON.stringify({foo: ['a', 'b']}),
      `{"foo":["a","b"]}`);
    
39.3.1.2. 结果:一条缩进线的树
  • 如果你为space提供一个非负整数(我们在这里忽略replacer,后面的 解释了),那么.stringify()会返回一个或多个行和每个space空格的缩进嵌套水平:
    assert.equal(
    JSON.stringify({foo: ['a', 'b']}, null, 2),
    `{
      "foo": [
        "a",
        "b"
      ]
    }`);
    
39.3.1.3. 有关 JavaScript 值如何字符串化的详细信息
  • 支持的原始值按预期进行字符串化:
    > JSON.stringify('abc')
    '"abc"'
    > JSON.stringify(123)
    '123'
    > JSON.stringify(null)
    'null'
    
  • 非有限数字(包括NaN)被字符串化为'null'
    > JSON.stringify(NaN)
    'null'
    > JSON.stringify(Infinity)
    'null'
    
  • 不支持的原始值被字符串化为undefined
    > JSON.stringify(undefined)
    undefined
    > JSON.stringify(Symbol())
    undefined
    
  • 函数字符串化为undefined
    > JSON.stringify(() => {})
    undefined
    
  • 在数组中,将字符串化为undefined的元素被字符串化为'null'
    > JSON.stringify([undefined, 123, () => {}])
    '[null,123,null]'
    
  • 在一个对象(既不是数组也不是函数)中,将跳过其值将被字符串化为undefined的属性:
    > JSON.stringify({a: Symbol(), b: true})
    '{"b":true}'
    
  • 如果一个对象(可能是一个数组或一个函数)有一个方法.toJSON(),那么该方法的结果将被字符串化,而不是该对象。例如,日期有一个返回字符串的方法.toJSON()
    > JSON.stringify({toJSON() {return true}})
    'true'
    > JSON.stringify(new Date(2999, 11, 31))
    '"2999-12-30T23:00:00.000Z"'
    

39.3.2. JSON.parse(text, reviver?)

  • .parse()将 JSON text转换为 JavaScript 值:
    > JSON.parse('{"foo":["a","b"]}')
    { foo: [ 'a', 'b' ] }
    

39.3.3. 示例:转换为 JSON 和从 JSON 转换

  • 以下类演示了一种实现 JSON 转换的技术:
    class Point {
      static fromJson(jsonObj) {
        return new Point(jsonObj.x, jsonObj.y);
      }
      constructor(x, y) {
        this.coord = [x, y];
      }
      toJSON() {
        const [x, y] = this.coord;
        return {x, y};
      }
    }
    assert.equal(
      JSON.stringify(new Point(3, 5)),
      '{"x":3,"y":5}');
    assert.deepEqual(
      Point.fromJson(JSON.parse('{"x":3,"y":5}')),
      new Point(3, 5));
    

前面提到的方法.toJSON()用于字符串化Point的实例。

练习:将对象转换为 JSON

exercises/json/to_from_json_test.js

39.4. 配置字符串化或解析的内容(高级)

字符串化或解析的内容可以配置如下:

  • .stringify()具有可选参数replacer,其中包含:
    • 具有属性名称的 Array。在对对象(可能是嵌套的)进行字符串化时,只会考虑这些属性,所有其他属性都将被忽略。
    • 一个 _ 值 visitor_ - 一个可以在字符串化之前转换 JavaScript 值的函数。
  • .parse()具有可选参数reviver - 一个值访问者,可以在返回之前转换已解析的 JSON 数据。

39.4.. .stringfy():指定对象应具有的唯一属性

  • 如果.stringify()的第二个参数是一个数组,那么结果中只包含在数组中提到其名称的对象属性:
    const obj = {
      a: 1,
      b: {
        c: 2,
        d: 3,
      }
    };
    assert.equal(
      JSON.stringify(obj, ['b', 'c']),
      '{"b":{"c":2}}');
    

39.4.2. .stringify().parse():重视访客

我称之为 _ 值访问者 _ 是一个转换 JavaScript 值(复合或原子)的函数:

  • JSON.stringify()在其接收到的 JavaScript 值之前调用其参数replacer中的值 visitor。
  • 解析 JSON 数据后,JSON.parse()在其参数reviver中调用值 visitor。

JavaScript 值的转换如下:值为原子值或复合值,并包含更多值(嵌套在数组和对象中)。原子值或嵌套值一次一个地馈送到值 vistor。根据访问者返回的内容,将删除,更改或保留当前值。

  • 值访问者具有以下类型签名:
    type ValueVisitor = (this: object, key: string, value: any) => any;
    
  • 参数是:
    • value:当前值。
    • this:当前值的父级。根值r的父级是{'': r}
    • key:其父级内当前值的键或索引。空字符串用于根值。
  • 值访问者可以返回:
    • value:表示不会有任何变化。
    • 不同的值x:导致valuex取代。
    • undefined:导致value被移除。

39.4.3. 示例:访问值

  • 以下代码演示了值访问者查看值的顺序。
    const log = [];
    function valueVisitor(key, value) {
      log.push({key, value, this: this});
      return value; // no change
    }
    const root = {
      a: 1,
      b: {
        c: 2,
        d: 3,
      }
    };
    JSON.stringify(root, valueVisitor);
    assert.deepEqual(log, [
      { key: '',  value: root,   this: { '': root } },
      { key: 'a', value: 1,      this: root         },
      { key: 'b', value: root.b, this: root         },
      { key: 'c', value: 2,      this: root.b       },
      { key: 'd', value: 3,      this: root.b       },
    ]);
    
  • 如您所见,.stringify()从上到下访问值(根首先,最后留下)。相反,.parse()访问自下而上的值(先离开,最后一根)。

39.4.4. 示例:对不支持的值进行字符串化

  • .stringify()对正则表达式对象没有特殊支持 - 它将它们字符串化为就像它们是普通对象一样:
    const obj = {
      name: 'abc',
      regex: /abc/ui,
    };
    assert.equal(
      JSON.stringify(obj),
      '{"name":"abc","regex":{}}');
    
  • 我们可以通过替换器解决这个问题:
    function replacer(key, value) {
      if (value instanceof RegExp) {
        return {
          __type__: 'RegExp',
          source: value.source,
          flags: value.flags,
        };
      } else {
        return value; // no change
      }
    }
    assert.equal(
    JSON.stringify(obj, replacer, 2),
    `{
      "name": "abc",
      "regex": {
        "__type__": "RegExp",
        "source": "abc",
        "flags": "iu"
      }
    }`);
    

39.4.5. 示例:解析不支持的值

  • .parse()上一节的结果,我们需要一个复活者:
    function reviver(key, value) {
      // Very simple check
      if (value && value.__type__ === 'RegExp') {
        return new RegExp(value.source, value.flags);
      } else {
        return value;
      }
    }
    const str = `{
      "name": "abc",
      "regex": {
        "__type__": "RegExp",
        "source": "abc",
        "flags": "iu"
      }
    }`;
    assert.deepEqual(
      JSON.parse(str, reviver),
      {
        name: 'abc',
        regex: /abc/ui,
      });
    

39.5. 常问问题

39.5.1. 为什么 JSON 不支持评论?

道格拉斯·克罗克福德在中解释了为什么从 2012 年 5 月 1 日开始发布一条 Google+信息

我从 JSON 中删除了注释,因为我看到有人使用它们来保存解析指令,这种做法会破坏互操作性。我知道缺乏评论会让一些人感到悲伤,但事实并非如此。

假设您使用 JSON 来保留您想要注释的配置文件。继续,插入您喜欢的所有评论。然后将它传递给 JSMin [JavaScript 的一个 minifier],然后再将它传递给你的 JSON 解析器。

39.6. 快速参考:JSON

值访客签名type ValueVisitor = (this: object, key: string, value: any) => any;

JSON

  • .stringify(value: any, replacer?: ValueVisitor, space?: string | number): string ^[ES5]^ 将value转换为 JSON 字符串。本章前面解释了参数replacer 。参数space的工作原理如下:
    • 如果省略space.stringify()将返回单行文本。
      assert.equal(
      JSON.stringify({foo: 1, bar: [2, 3]}),
      '{"foo":1,"bar":[2,3]}');
      
    • 如果space是一个数字,.stringify()返回一行或多行,并按每个嵌套级别的space空格缩进。
      assert.equal(
      JSON.stringify({foo: 1, bar: [2, 3]}, null, 2),
      `{
        "foo": 1,
        "bar": [
          2,
          3
        ]
      }`);
      
    • 如果space是一个字符串,则用于缩进。
      assert.equal(
      JSON.stringify({foo: 1, bar: [2, 3]}, null, '>>'),
      `{
      >>"foo": 1,
      >>"bar": [
      >>>>2,
      >>>>3
      >>]
      }`);
      
  • .stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string ^[ES5]^ 如果replacer是一个数组,则结果只包括其名称在数组中提及的对象属性。
    > JSON.stringify({foo: 1, bar: 2}, ['foo'])
    '{"foo":1}'
    
  • .parse(text: string, reviver?: ValueVisitor): any ^[ES5]^ 解析text中的 JSON 并返回 JavaScript 值。本章前面解释了参数reviver
    assert.deepEqual(
      JSON.parse('{"a":true,"b":[1,2]}'),
      { a: true, b: [1,2] }
    );