原文链接: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 numberalert( JSON.stringify(1) ) // 1// a string in JSON is still a string, but double-quotedalert( JSON.stringify('test') ) // testalert( JSON.stringify(true) ); // truealert( 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 roomroom.occupiedBy = meetup; // room references meetupJSON.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 引用了 meetupalert( 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 引用了 meetupalert( 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 引用了 meetupalert( JSON.stringify(meetup, function replacer(key, value) {alert(`${key}: ${value}`); // to see what replacer getsreturn (key == 'occupiedBy') ? undefined : value;}));/* key:value 对会进入到 replacer:: [object Object]title: Conferenceparticipants: [object Object],[object Object]0: [object Object]name: John1: [object Object]name: Aliceplace: [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) ); // 23alert( 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 的时候自动被调用。
(完)
