假设我们有一个复杂的对象,并且我们希望将其转换成一个字符串,以便通过网络发送它,或者仅仅为了记录目的而输出它。
自然地,这样的字符串应该包含所有重要的属性。
我们可以像这样实现转换
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
会调用它。
评论
<code>
标签,对于多行代码 – 将其包装在<pre>
标签中,对于超过 10 行的代码 – 使用沙箱 (plnkr、jsbin、codepen…)