原文链接:http://javascript.info/json,translate with ❤️ by zhangbao.

假设我们有一个复杂的对象,我们想把它转换成一个字符串,把它发送到一个网络上,或者仅仅是为了记录目的而输出它。

当然,这样的字符串应该包含所有重要的属性。

我们可以像这样实现转换:

  1. let user = {
  2. name: "John",
  3. age: 30,
  4. toString() {
  5. return `{name: "${this.name}", age: ${this.age}}`;
  6. }
  7. };
  8. alert(user); // {name: "John", age: 30}

但是在开发的过程中,添加了新的属性,旧的属性被重命名或删除。每次更新这样的 toString 会成为一种痛苦。我们可以尝试对其中的属性进行循环,但是如果对象是复杂的,并且在属性中有嵌套的对象呢?我们还需要实现他们的转换。而且,如果我们把对象发送到网络上,那么我们还需要提供代码来“读取”我们在接收端上的对象。

幸运的是,没有必要编写代码来处理所有这些。这项任务已经解决了。

JSON.stringify

JSON(JavaScript 对象表示法)是一种表示值和对象的通用格式。它被描述为 RFC 4627 标准。最初它是为 JavaScript 编写的,但许多其他语言也有库来处理它。因此,当客户端使用 JavaScript 时,使用 JSON 进行数据交换是很容易的,而服务器端使用 Ruby/PHP/Java/ 之类语言编写的。

JavaScript 提供了方法:

  • JSON.stringify 将对象转换为 JSON。

  • JSON.parse 将 JSON 转换为对象。

例如,我们 JSON.stringify 一个学生:

  1. let student = {
  2. name: 'John',
  3. age: 30,
  4. isAdmin: false,
  5. courses: ['html', 'css', 'js'],
  6. wife: null
  7. };
  8. let json = JSON.stringify(student);
  9. alert(typeof json); // we've got a string!
  10. alert(json);
  11. /* JSON-encoded object:
  12. {
  13. "name": "John",
  14. "age": 30,
  15. "isAdmin": false,
  16. "courses": ["html", "css", "js"],
  17. "wife": null
  18. }
  19. */

JSON,stringify 方法将对象转换为字符串。

结果 json 这个字符串变量称为 JSON 编码或序列化或排列对象。我们已经准备好将它发送到网络上,或者将其放入普通的数据存储中。

请注意,JSON 编码对象与对象字面量有几个重要的区别:

  • 字符串使用双引号。在 JSON 中不存在单引号和反引号。因此,’John’ 变为 “John”。

  • 对象的属性名也使用双引号括起来,这是必须的。因此,age: 30 变成 “age”: 30。

JSON.stringify 也可以用在原始类型值上。

原生支持的 JSON 类型有:

  • 对象 {…}

  • 数组 […]

  • 原始类型

    • 布尔值 true,false,

    • 数值

    • 字符串,

    • null。

例如:

  1. // a number in JSON is just a number
  2. alert( JSON.stringify(1) ) // 1
  3. // a string in JSON is still a string, but double-quoted
  4. alert( JSON.stringify('test') ) // test
  5. alert( JSON.stringify(true) ); // true
  6. alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]

JSON 是适应于数据传输的跨语言规范,因此特定于 JavaScript 语言的一些属性会被 JSON.stringify 方法忽略。

也就是:

  • 函数类型属性(方法)。

  • Symbol 属性。

  • 属性值是 undefined 的属性。

  1. let user = {
  2. sayHi() { // 忽略
  3. alert("Hello");
  4. },
  5. [Symbol("id")]: 123, // 忽略
  6. something: undefined // 忽略
  7. };
  8. alert( JSON.stringify(user) ); // {} (空对象)

通常很好。如果这不是我们想要的,那么很快我们就会看到如何定制这个过程。

最重要的是,嵌套的对象会被自动地支持和转换。

例如:

  1. let meetup = {
  2. title: "Conference",
  3. room: {
  4. number: 23,
  5. participants: ["john", "ann"]
  6. }
  7. };
  8. alert( JSON.stringify(meetup) );
  9. /* 整个字符串化后的结构:
  10. {
  11. "title":"Conference",
  12. "room":{"number":23,"participants":["john","ann"]},
  13. }
  14. */

重要的限制:必须没有循环引用。

例如:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. participants: ["john", "ann"]
  7. };
  8. meetup.place = room; // meetup references room
  9. room.occupiedBy = meetup; // room references meetup
  10. JSON.stringify(meetup); // Error: Converting circular structure to JSON

这里的转换会失败,因为存在循环引用:room.occupiedBy 引用了 meetup,meetup.place 引用了 room:

JSON 方法,toJSON - 图1

排除和转换:replacer

JSON.stringify 的完整语法是这样的:

  1. let json = JSON.stringify(value[, replacer, space])

value

要编码的值。

replacer

编码的属性数组或映射函数。

space

用于格式化的空格数量。

多数时间,JSON.stringify 仅使用第一个参数就够了。但是如果我们需要对替换过程进行微调,像过滤掉循环引用,那么就可以使用 JSON.stringify 的第二个参数。

如果我们传递了一个由属性名组成的数组,那么表示只有数组里包含得这些属性会被编码:

例如:

  1. let room = {};
  2. let meetup = {
  3. title: "Conference",
  4. participants: [{name: "John"}, {name: "Alice"}],
  5. place: room // meetup 引用了 room
  6. };
  7. room.number = 23;
  8. room.occupiedBy = meetup; // room 引用了 meetup
  9. alert( JSON.stringify(meetup, ['title', 'participants']) );
  10. // {"title":"Conference","participants":[{},{}]}

我们的要求可能太过严格了。这里列举的属性对整个对象结构都是起作用的。因此会发现 participants 是空的,因为 name 没有在属性列表中。

下面我们编码除(引发循环引用的) room.occupiedBy 属性之外的其他属性:

  1. let room = {};
  2. let meetup = {
  3. title: "Conference",
  4. participants: [{name: "John"}, {name: "Alice"}],
  5. place: room // meetup 引用了 room
  6. };
  7. room.number = 23;
  8. room.occupiedBy = meetup; // room 引用了 meetup
  9. alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
  10. /*
  11. {
  12. "title":"Conference",
  13. "participants":[{"name":"John"},{"name":"Alice"}],
  14. "place":{"number":23}
  15. }
  16. */

现在除 occupiedBy 之外的其他属性都被序列化了,但是属性列表相当长。

而幸运的是,我们可以使用一个函数而不是一个数组来作为 replacer 使用。

函数会在每个 (key, value) 对上调用,并且返回“替换后”的值,它将代替原来的那个。

在这种情况下,我们可以“原样”返回除 occupiedBy 属性之外的其他 value(注意这里的 value 是序列化之后的值,总是一个字符串)。忽视 occupiedBy 属性,我们只需在代码里返回 undefined 就 OK 了:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. participants: [{name: "John"}, {name: "Alice"}],
  7. place: room // meetup 引用了 room
  8. };
  9. room.occupiedBy = meetup; // room 引用了 meetup
  10. alert( JSON.stringify(meetup, function replacer(key, value) {
  11. alert(`${key}: ${value}`); // to see what replacer gets
  12. return (key == 'occupiedBy') ? undefined : value;
  13. }));
  14. /* key:value 对会进入到 replacer:
  15. : [object Object]
  16. title: Conference
  17. participants: [object Object],[object Object]
  18. 0: [object Object]
  19. name: John
  20. 1: [object Object]
  21. name: Alice
  22. place: [object Object]
  23. number: 23
  24. */

需要注意的是,replacer 函数会递归遍历内嵌对象和数组成员里的每个键值对。replacer 中的 this 指向包含当前属性的对象。

第一次调用比较特别。是一个特殊的“包裹对象”:{“”: meetup}。也就是说,第一个 (key, value) 对有一个空的 key,值是整个的目标对象。这就是为什么上例中的第一行显示 “:[object Object]”。

这样做的目的是使用 replacer 提供尽可能多的权力:如果有必要,它有机会分析和替换/跳过整个对象。

格式化:spacer

JSON.stringify(value, replacer, spaces) 是用于指定输出格式中的空格缩进数量。

以前,所有的字符串华对象都没有缩进和额外空格,这对在网络上发送一个对象是极好的。spacer 参数会递归调用得到一个良好输出的结果。

这里的 spacer = 2 告诉 JavaScript 在多个行中显示嵌套的对象,其中包含两个空格的缩进:

  1. let user = {
  2. name: "John",
  3. age: 25,
  4. roles: {
  5. isAdmin: false,
  6. isEditor: true
  7. }
  8. };
  9. alert(JSON.stringify(user, null, 2));
  10. /* 两个空格的缩进:
  11. {
  12. "name": "John",
  13. "age": 25,
  14. "roles": {
  15. "isAdmin": false,
  16. "isEditor": true
  17. }
  18. }
  19. */
  20. /*JSON.stringify(user, null, 4) 的结果是更加缩进的字符串输出:
  21. {
  22. "name": "John",
  23. "age": 25,
  24. "roles": {
  25. "isAdmin": false,
  26. "isEditor": true
  27. }
  28. }
  29. */

spaces 参数仅用于日志记录和美化输出的目的。

自定义“toJSON”

toString 方法用于字符串转换,一个对象可以提供 toJSON 方法为了 JSON 转换。JSON.stringify 发现对象如果有这个方法的话,会自动调用。

例如:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. date: new Date(Date.UTC(2017, 0, 1)),
  7. room
  8. };
  9. alert( JSON.stringify(meetup) );
  10. /*
  11. {
  12. "title":"Conference",
  13. "date":"2017-01-01T00:00:00.000Z", // (1)
  14. "room": {"number":23} // (2)
  15. }
  16. */

我们看到 date (1) 变成了字符串。这是因为所有的日期对象都有内置的 toJSON 方法,返回这个类型实例对应的字符串表示。

现在我们为 room 对象添加一个自定义 toJSON 方法:

  1. let room = {
  2. number: 23,
  3. toJSON() {
  4. return this.number;
  5. }
  6. };
  7. let meetup = {
  8. title: "Conference",
  9. room
  10. };
  11. alert( JSON.stringify(room) ); // 23
  12. alert( JSON.stringify(meetup) );
  13. /*
  14. {
  15. "title":"Conference",
  16. "room": 23
  17. }
  18. */

可以看到,toJSON 既用于直接调用 JSON.stringify(room),也适用于嵌套对象。

JSON.parse

解码 JSON 字符串,我们需要使用另一个名为 JSON.parse 的方法。

语法是:

  1. let value = JSON.parse(str[, reviver]);

str

要解析的 JSON 字符串。

reviver

可选的 function(key, value) 会在每个 (key, value) 对上被调用,并且可以转换值。

例如:

  1. // 字符串化后的数组
  2. let numbers = "[0, 1, 2, 3]";
  3. numbers = JSON.parse(numbers);
  4. alert( numbers[1] ); // 1

或者是内嵌对象:

  1. let user = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
  2. user = JSON.parse(user);
  3. alert( user.friends[1] ); // 1

JSON 可能是必要的复杂,对象和数组可以包括其他对象和数组,但他们必须遵守这种格式。

以下是手工编写的 JSON 中的典型错误(有时我们不得不将其编写为调试目的):

  1. let json = `{
  2. name: "John", // 错误: 属性名无引号
  3. "surname": 'Smith', // 错误: 属性值使用了单引号 (必须是双引号)
  4. 'isAdmin': false // 错误: 属性名使用了单引号 (必须是双引号)
  5. "birthday": new Date(2000, 2, 3), // 错误: 不允许出现 "new", 只能使用单纯的值
  6. "friends": [0,1,2,3] // 这个是 OK 的
  7. }`;

此外,JSON 不支持注释。向 JSON 添加注释使其无效。

还有另一种格式名为 JSON5,它允许未引用的键、注释等。但这是一个独立的库,而不是语言的规范。

普通的 JSON 是严格的,不是因为它的开发人员是懒惰的,而是允许简单、可靠和快速的解析算法实现。

使用 reviver

想象一下,我们从服务器上得到了一个字符串的 meetup 对象。

看起来是这样的:

  1. // title: (meetup title), date: (meetup date)
  2. let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

现在我们需要反序列化它,重新回到 JavaScript 对象。

我们调用 JSON.parse:

  1. let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
  2. let meetup = JSON.parse(str);
  3. alert( meetup.date.getDate() ); // Error!

天哪,出错了!

meetup.date 的值是一个字符串,不是一个 Date 对象。JSON.parse 怎么知道把它转换成 Date 呢?

接下来我们使用的 JSON.parse 的 reviving 函数,将除了将 date 成为 Date 之外,其他最有的值都“原样输出”:

  1. let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
  2. let meetup = JSON.parse(str, function(key, value) {
  3. if (key == 'date') return new Date(value);
  4. return value;
  5. });
  6. alert( meetup.date.getDate() ); // 现在 OK 了!

顺便说一下,这也适用于嵌套的对象:

  1. let schedule = `{
  2. "meetups": [
  3. {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
  4. {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  5. ]
  6. }`;
  7. schedule = JSON.parse(schedule, function(key, value) {
  8. if (key == 'date') return new Date(value);
  9. return value;
  10. });
  11. alert( schedule.meetups[1].date.getDate() ); // works!

总结

  • JSON是一种数据格式,它有自己的独立标准和大多数编程语言实现的库。

  • JSON 支持纯对象、数组、字符串、数值、布尔值和 null。

  • JavaScript 提供了 JSON.stringify 方法将数据序列化成 JSON 字符串和 JSON.parse 从 JSON 字符串读取解析出对象。

  • 这两种方法都支持转换功能,用于智能读取/写入。

  • 如果一个对象具有 toJSON 方法,就会在使用 JSON.stringify 的时候自动被调用。

(完)