原文链接:http://javascript.info/json,translate with ❤️ by zhangbao.
假设我们有一个复杂的对象,我们想把它转换成一个字符串,把它发送到一个网络上,或者仅仅是为了记录目的而输出它。
当然,这样的字符串应该包含所有重要的属性。
我们可以像这样实现转换:
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 时,使用 JSON 进行数据交换是很容易的,而服务器端使用 Ruby/PHP/Java/ 之类语言编写的。
JavaScript 提供了方法:
JSON.stringify 将对象转换为 JSON。
JSON.parse 将 JSON 转换为对象。
例如,我们 JSON.stringify 一个学生:
let student = {
name: 'John',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
wife: 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"],
"wife": null
}
*/
JSON,stringify 方法将对象转换为字符串。
结果 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 是适应于数据传输的跨语言规范,因此特定于 JavaScript 语言的一些属性会被 JSON.stringify 方法忽略。
也就是:
函数类型属性(方法)。
Symbol 属性。
属性值是 undefined 的属性。
let user = {
sayHi() { // 忽略
alert("Hello");
},
[Symbol("id")]: 123, // 忽略
something: undefined // 忽略
};
alert( JSON.stringify(user) ); // {} (空对象)
通常很好。如果这不是我们想要的,那么很快我们就会看到如何定制这个过程。
最重要的是,嵌套的对象会被自动地支持和转换。
例如:
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}
};
alert( JSON.stringify(meetup) );
/* 整个字符串化后的结构:
{
"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:
排除和转换:replacer
JSON.stringify 的完整语法是这样的:
let json = JSON.stringify(value[, replacer, space])
value
要编码的值。
replacer
编码的属性数组或映射函数。
space
用于格式化的空格数量。
多数时间,JSON.stringify 仅使用第一个参数就够了。但是如果我们需要对替换过程进行微调,像过滤掉循环引用,那么就可以使用 JSON.stringify 的第二个参数。
如果我们传递了一个由属性名组成的数组,那么表示只有数组里包含得这些属性会被编码:
例如:
let room = {};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.number = 23;
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}
我们的要求可能太过严格了。这里列举的属性对整个对象结构都是起作用的。因此会发现 participants 是空的,因为 name 没有在属性列表中。
下面我们编码除(引发循环引用的) room.occupiedBy 属性之外的其他属性:
let room = {};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.number = 23;
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
"title":"Conference",
"participants":[{"name":"John"},{"name":"Alice"}],
"place":{"number":23}
}
*/
现在除 occupiedBy 之外的其他属性都被序列化了,但是属性列表相当长。
而幸运的是,我们可以使用一个函数而不是一个数组来作为 replacer 使用。
函数会在每个 (key, value) 对上调用,并且返回“替换后”的值,它将代替原来的那个。
在这种情况下,我们可以“原样”返回除 occupiedBy 属性之外的其他 value(注意这里的 value 是序列化之后的值,总是一个字符串)。忽视 occupiedBy 属性,我们只需在代码里返回 undefined 就 OK 了:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`); // to see what replacer gets
return (key == 'occupiedBy') ? undefined : value;
}));
/* key:value 对会进入到 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
*/
需要注意的是,replacer 函数会递归遍历内嵌对象和数组成员里的每个键值对。replacer 中的 this 指向包含当前属性的对象。
第一次调用比较特别。是一个特殊的“包裹对象”:{“”: meetup}。也就是说,第一个 (key, value) 对有一个空的 key,值是整个的目标对象。这就是为什么上例中的第一行显示 “:[object Object]”。
这样做的目的是使用 replacer 提供尽可能多的权力:如果有必要,它有机会分析和替换/跳过整个对象。
格式化:spacer
JSON.stringify(value, replacer, spaces) 是用于指定输出格式中的空格缩进数量。
以前,所有的字符串华对象都没有缩进和额外空格,这对在网络上发送一个对象是极好的。spacer 参数会递归调用得到一个良好输出的结果。
这里的 spacer = 2 告诉 JavaScript 在多个行中显示嵌套的对象,其中包含两个空格的缩进:
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
alert(JSON.stringify(user, null, 2));
/* 两个空格的缩进:
{
"name": "John",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
/*JSON.stringify(user, null, 4) 的结果是更加缩进的字符串输出:
{
"name": "John",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
spaces 参数仅用于日志记录和美化输出的目的。
自定义“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 对象添加一个自定义 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),也适用于嵌套对象。
JSON.parse
解码 JSON 字符串,我们需要使用另一个名为 JSON.parse 的方法。
语法是:
let value = JSON.parse(str[, reviver]);
str
要解析的 JSON 字符串。
reviver
可选的 function(key, value) 会在每个 (key, value) 对上被调用,并且可以转换值。
例如:
// 字符串化后的数组
let numbers = "[0, 1, 2, 3]";
numbers = JSON.parse(numbers);
alert( numbers[1] ); // 1
或者是内嵌对象:
let user = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
user = JSON.parse(user);
alert( user.friends[1] ); // 1
JSON 可能是必要的复杂,对象和数组可以包括其他对象和数组,但他们必须遵守这种格式。
以下是手工编写的 JSON 中的典型错误(有时我们不得不将其编写为调试目的):
let json = `{
name: "John", // 错误: 属性名无引号
"surname": 'Smith', // 错误: 属性值使用了单引号 (必须是双引号)
'isAdmin': false // 错误: 属性名使用了单引号 (必须是双引号)
"birthday": new Date(2000, 2, 3), // 错误: 不允许出现 "new", 只能使用单纯的值
"friends": [0,1,2,3] // 这个是 OK 的
}`;
此外,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 的 reviving 函数,将除了将 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() ); // 现在 OK 了!
顺便说一下,这也适用于嵌套的对象:
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 的时候自动被调用。
(完)