2024 年 1 月 24 日

JSON 方法,toJSON

假设我们有一个复杂的对象,并且我们希望将其转换成一个字符串,以便通过网络发送它,或者仅仅为了记录目的而输出它。

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

我们可以像这样实现转换

let user = {
  name: "John",
  age: 30,

  toString() {
    return `{name: "${this.name}", age: ${this.age}}`;
  }
};

alert(user); // {name: "John", age: 30}

…但在开发过程中,添加了新属性,重命名并删除了旧属性。每次更新这样的 toString 都会变成一种痛苦。我们可以尝试循环遍历其中的属性,但如果对象很复杂,并且在属性中嵌套了对象,该怎么办?我们还需要实现它们的转换。

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

JSON.stringify

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

JavaScript 提供了方法

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

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

let student = {
  name: 'John',
  age: 30,
  isAdmin: false,
  courses: ['html', 'css', 'js'],
  spouse: null
};

let json = JSON.stringify(student);

alert(typeof json); // we've got a string!

alert(json);
/* JSON-encoded object:
{
  "name": "John",
  "age": 30,
  "isAdmin": false,
  "courses": ["html", "css", "js"],
  "spouse": null
}
*/

方法 JSON.stringify(student) 获取对象并将其转换为字符串。

生成的 json 字符串称为JSON 编码序列化字符串化编组对象。我们准备通过网络发送它或将其放入纯数据存储中。

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

  • 字符串使用双引号。JSON 中没有单引号或反引号。因此 'John' 变为 "John"
  • 对象属性名称也使用双引号。这是强制性的。因此 age:30 变为 "age":30

JSON.stringify 也可以应用于基元。

JSON 支持以下数据类型

  • 对象 { ... }
  • 数组 [ ... ]
  • 基元
    • 字符串,
    • 数字,
    • 布尔值 true/false
    • null.

例如

// a number in JSON is just a number
alert( JSON.stringify(1) ) // 1

// a string in JSON is still a string, but double-quoted
alert( JSON.stringify('test') ) // "test"

alert( JSON.stringify(true) ); // true

alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]

JSON 是仅限数据的语言无关规范,因此 JSON.stringify 会跳过一些特定于 JavaScript 的对象属性。

  • 函数属性(方法)。
  • 符号键和值。
  • 存储 undefined 的属性。
let user = {
  sayHi() { // ignored
    alert("Hello");
  },
  [Symbol("id")]: 123, // ignored
  something: undefined // ignored
};

alert( JSON.stringify(user) ); // {} (empty object)

通常情况下,这很好。如果这不是我们想要的,那么我们很快就会看到如何自定义此过程。

最棒的是,它支持嵌套对象并自动转换它们。

例如

let meetup = {
  title: "Conference",
  room: {
    number: 23,
    participants: ["john", "ann"]
  }
};

alert( JSON.stringify(meetup) );
/* The whole structure is stringified:
{
  "title":"Conference",
  "room":{"number":23,"participants":["john","ann"]},
}
*/

重要的限制:不能有循环引用。

例如

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: ["john", "ann"]
};

meetup.place = room;       // meetup references room
room.occupiedBy = meetup; // room references meetup

JSON.stringify(meetup); // Error: Converting circular structure to JSON

此处,转换失败,因为存在循环引用:room.occupiedBy 引用 meetup,而 meetup.place 引用 room

排除和转换:替换器

JSON.stringify 的完整语法为

let json = JSON.stringify(value[, replacer, space])
value
要编码的一个值。
替换器
要编码的属性数组或映射函数 function(key, value)
空格
用于格式化的空格量

大多数情况下,JSON.stringify 仅与第一个参数一起使用。但是,如果我们需要微调替换过程,比如过滤掉循环引用,可以使用 JSON.stringify 的第二个参数。

如果我们向其传递一个属性数组,则仅编码这些属性。

例如

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}

我们在这里可能过于严格了。属性列表应用于整个对象结构。因此,participants 中的对象为空,因为 name 不在列表中。

让我们在列表中包含除 room.occupiedBy 之外的每个属性,这将导致循环引用

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
  "title":"Conference",
  "participants":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/

现在除了 occupiedBy 之外的一切都已序列化。但属性列表很长。

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

该函数将针对每个 (key, value) 对调用,并应返回“替换”值,该值将用于替换原始值。或者,如果要跳过该值,则返回 undefined

在我们的例子中,我们可以为除 occupiedBy 之外的所有内容返回 value “原样”。要忽略 occupiedBy,以下代码返回 undefined

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));

/* key:value pairs that come to replacer:
:             [object Object]
title:        Conference
participants: [object Object],[object Object]
0:            [object Object]
name:         John
1:            [object Object]
name:         Alice
place:        [object Object]
number:       23
occupiedBy: [object Object]
*/

请注意,replacer 函数获取每个键/值对,包括嵌套对象和数组项。它以递归方式应用。replacer 中的 this 的值是包含当前属性的对象。

第一个调用是特殊的。它使用一个特殊的“包装对象”进行:{"": meetup}。换句话说,第一个 (key, value) 对有一个空键,而值是整个目标对象。这就是为什么在上面的示例中第一行是 ":[object Object]"

这个想法是为 replacer 提供尽可能多的功能:它有机会分析和替换/跳过整个对象,如果需要的话。

格式化:空格

JSON.stringify(value, replacer, space) 的第三个参数是要用于漂亮格式化的空格数。

以前,所有字符串化对象都没有缩进和额外的空格。如果我们想通过网络发送一个对象,这是可以的。space 参数专门用于漂亮的输出。

这里的 space = 2 告诉 JavaScript 在多行上显示嵌套对象,并在对象内缩进 2 个空格

let user = {
  name: "John",
  age: 25,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};

alert(JSON.stringify(user, null, 2));
/* two-space indents:
{
  "name": "John",
  "age": 25,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/

/* for JSON.stringify(user, null, 4) the result would be more indented:
{
    "name": "John",
    "age": 25,
    "roles": {
        "isAdmin": false,
        "isEditor": true
    }
}
*/

第三个参数也可以是一个字符串。在这种情况下,该字符串用于缩进,而不是空格数。

space 参数仅用于记录和美观输出。

自定义“toJSON”

与用于字符串转换的 toString 类似,对象可以提供 toJSON 方法用于转换为 JSON。JSON.stringify 在可用时会自动调用它。

例如

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  date: new Date(Date.UTC(2017, 0, 1)),
  room
};

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "date":"2017-01-01T00:00:00.000Z",  // (1)
    "room": {"number":23}               // (2)
  }
*/

在这里我们可以看到 date (1) 变成了一个字符串。这是因为所有日期都具有内置的 toJSON 方法,该方法返回此类字符串。

现在让我们为我们的对象 room (2) 添加一个自定义 toJSON

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};

let meetup = {
  title: "Conference",
  room
};

alert( JSON.stringify(room) ); // 23

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "room": 23
  }
*/

正如我们所看到的,toJSON 既用于直接调用 JSON.stringify(room),也用于 room 嵌套在另一个编码对象中的情况。

JSON.parse

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

语法

let value = JSON.parse(str[, reviver]);
str
要解析的 JSON 字符串。
reviver
可选函数 (key,value),它将针对每个 (key, value) 对调用,并且可以转换该值。

例如

// stringified array
let numbers = "[0, 1, 2, 3]";

numbers = JSON.parse(numbers);

alert( numbers[1] ); // 1

或对于嵌套对象

let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';

let user = JSON.parse(userData);

alert( user.friends[1] ); // 1

JSON 可以复杂到任何程度,对象和数组可以包含其他对象和数组。但它们必须遵循相同的 JSON 格式。

以下是手写 JSON 中常见的错误(有时我们不得不为了调试目的而编写它)

let json = `{
  name: "John",                     // mistake: property name without quotes
  "surname": 'Smith',               // mistake: single quotes in value (must be double)
  'isAdmin': false                  // mistake: single quotes in key (must be double)
  "birthday": new Date(2000, 2, 3), // mistake: no "new" is allowed, only bare values
  "friends": [0,1,2,3]              // here all fine
}`;

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

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

常规 JSON 之所以如此严格,并不是因为其开发者偷懒,而是为了允许对解析算法进行轻松、可靠且非常快速的实现。

使用 reviver

想象一下,我们从服务器获取了一个字符串化的 meetup 对象。

它看起来像这样

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

…现在我们需要对其进行反序列化,将其转换回 JavaScript 对象。

让我们通过调用 JSON.parse 来实现

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str);

alert( meetup.date.getDate() ); // Error!

糟糕!一个错误!

meetup.date 的值是一个字符串,而不是 Date 对象。JSON.parse 怎么知道它应该将该字符串转换为 Date

让我们将恢复函数作为第二个参数传递给 JSON.parse,该函数将返回所有值“按原样”,但 date 将变为 Date

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( meetup.date.getDate() ); // now works!

顺便说一下,这同样适用于嵌套对象

let schedule = `{
  "meetups": [
    {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
    {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  ]
}`;

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( schedule.meetups[1].date.getDate() ); // works!

总结

  • JSON 是一种数据格式,它有自己的独立标准和大多数编程语言的库。
  • JSON 支持普通对象、数组、字符串、数字、布尔值和 null
  • JavaScript 提供了方法 JSON.stringify 来序列化为 JSON 和 JSON.parse 来从 JSON 读取。
  • 这两种方法都支持转换器函数,用于智能读/写。
  • 如果对象有 toJSON,则 JSON.stringify 会调用它。

任务

重要性:5

user 转换为 JSON,然后将其读回另一个变量。

let user = {
  name: "John Smith",
  age: 35
};
let user = {
  name: "John Smith",
  age: 35
};

let user2 = JSON.parse(JSON.stringify(user));
重要性:5

在循环引用的简单情况下,我们可以通过名称从序列化中排除有问题的属性。

但有时我们不能只使用名称,因为它可能同时用于循环引用和普通属性。因此,我们可以通过其值检查属性。

编写 replacer 函数来字符串化所有内容,但删除引用 meetup 的属性

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  occupiedBy: [{name: "John"}, {name: "Alice"}],
  place: room
};

// circular references
room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  /* your code */
}));

/* result should be:
{
  "title":"Conference",
  "occupiedBy":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/
let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  occupiedBy: [{name: "John"}, {name: "Alice"}],
  place: room
};

room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  return (key != "" && value == meetup) ? undefined : value;
}));

/*
{
  "title":"Conference",
  "occupiedBy":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/

这里我们还需要测试 key=="" 以排除第一次调用,在第一次调用中 valuemeetup 是正常的。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码 – 将其包装在 <pre> 标签中,对于超过 10 行的代码 – 使用沙箱 (plnkrjsbincodepen…)