面向急切程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

45 创建和解析 JSON (JSON)



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

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

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

45.1 JSON 的发现和标准化

Douglas Crockford 于 2001 年在 json.org 上发布了 JSON 规范。他解释说

我发现了 JSON。我并不声称发明了 JSON,因为它本来就存在于自然界中。我所做的是我发现了它,我给它起了个名字,我描述了它的用途。我并不声称自己是第一个发现它的人;我知道至少在我之前一年就有人发现了它。我发现的最早的例子是,早在 1996 年,Netscape 就有人使用 JavaScript 数组字面量进行数据通信,这至少比我偶然想到这个想法早了五年。

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

45.1.1 JSON 的语法是固定的

引用 ECMA-404 标准

因为它非常简单,所以预计 JSON 语法永远不会改变。这赋予了 JSON 作为基础符号极大的稳定性。

因此,JSON 永远不会得到诸如可选的尾随逗号、注释或未加引号的键之类的改进——无论它们是否被认为是可取的。然而,这仍然为创建编译为纯 JSON 的 JSON 超集留下了空间。

45.2 JSON 语法

JSON 由以下 JavaScript 部分组成

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

45.3 使用 JSON API

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

45.3.1 JSON.stringify(data, replacer?, space?)

.stringify() 将 JavaScript data 转换为 JSON 字符串。在本节中,我们将忽略参数 replacer;它在 §45.4 “自定义字符串化和解析” 中进行了解释。

45.3.1.1 结果:单行文本

如果只提供第一个参数,则 .stringify() 返回单行文本

assert.equal(
  JSON.stringify({foo: ['a', 'b']}),
  '{"foo":["a","b"]}' );
45.3.1.2 结果:缩进行的树

如果为 space 提供一个非负整数,则 .stringify() 返回一行或多行,并按每级嵌套 space 个空格缩进

assert.equal(
JSON.stringify({foo: ['a', 'b']}, null, 2),
`{
  "foo": [
    "a",
    "b"
  ]
}`);
45.3.1.3 有关如何字符串化 JavaScript 数据的详细信息

原始值

对象

45.3.2 JSON.parse(text, reviver?)

.parse() 将 JSON text 转换为 JavaScript 值。在本节中,我们将忽略参数 reviver;它在 §45.4 “自定义字符串化和解析” 中进行了解释。

这是使用 .parse() 的示例

> JSON.parse('{"foo":["a","b"]}')
{ foo: [ 'a', 'b' ] }

45.3.3 示例:转换为 JSON 和从 JSON 转换

以下类实现了从(A 行)到(B 行)JSON 的转换。

class Point {
  static fromJson(jsonObj) { // (A)
    return new Point(jsonObj.x, jsonObj.y);
  }

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  toJSON() { // (B)
    return {x: this.x, y: this.y};
  }
}

  练习:将对象转换为 JSON 和从 JSON 转换

exercises/json/to_from_json_test.mjs

45.4 自定义字符串化和解析(高级)

字符串化和解析可以自定义如下

45.4.1 .stringfy():指定要字符串化的对象的属性

如果 .stringify() 的第二个参数是一个数组,则结果中只包含名称在其中提到的对象属性

const obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  }
};
assert.equal(
  JSON.stringify(obj, ['b', 'c']),
  '{"b":{"c":2}}');

45.4.2 .stringify().parse():值访问器

我所说的*值访问器*是一个转换 JavaScript 数据的函数

在本节中,JavaScript 数据被视为值树。如果数据是原子的,则它是一棵只有根节点的树。树中的所有值都一次一个地提供给值访问器。根据访问器返回的内容,当前值将被省略、更改或保留。

值访问器具有以下类型签名

type ValueVisitor = (key: string, value: any) => any;

参数是

值访问器可以返回

45.4.3 示例:访问值

以下代码显示了值访问器查看值的顺序

const log = [];
function valueVisitor(key, value) {
  log.push({this: this, key, value});
  return value; // no change
}

const root = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  }
};
JSON.stringify(root, valueVisitor);
assert.deepEqual(log, [
  { this: { '': root }, key: '',  value: root   },
  { this: root        , key: 'a', value: 1      },
  { this: root        , key: 'b', value: root.b },
  { this: root.b      , key: 'c', value: 2      },
  { this: root.b      , key: 'd', value: 3      },
]);

我们可以看到,JSON.stringify() 的替换器自顶向下访问值(根节点优先,叶节点最后)。朝这个方向进行的理由是我们正在将 JavaScript 值转换为 JSON 值。单个 JavaScript 对象可能会扩展为 JSON 兼容值的树。

相反,JSON.parse() 的还原器自底向上访问值(叶节点优先,根节点最后)。朝这个方向进行的理由是我们正在将 JSON 值组装成 JavaScript 值。因此,我们需要先转换部分,然后才能转换整体。

45.4.4 示例:字符串化不支持的值

JSON.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"
  }
}`);

45.4.5 示例:解析不支持的值

JSON.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,
  });

45.5 常见问题解答

45.5.1 为什么 JSON 不支持注释?

Douglas Crockford 在 2012 年 5 月 1 日的 Google+ 帖子 中解释了原因

我从 JSON 中删除了注释,因为我看到人们使用它们来保存解析指令,这种做法会破坏互操作性。我知道缺少注释会让一些人感到难过,但其实不必如此。

假设您正在使用 JSON 来保存配置文件,并且您希望对其进行注释。请随意插入您喜欢的任何注释。然后在将其交给 JSON 解析器之前,将其通过 JSMin [JavaScript 的压缩器] 进行管道传输。