前言
JSON 在编程生涯简直就是无处不在啊。
- 那么 JSON 是什么呢?
- 跟我们的 JavaScript 有什么关系呢?
- 在 JavaScript 中,我们如何处理 JSON 数据呢?
正文
一、JSON
JSON(JavaScript Object Natation)是一种轻量级的数据交换格式。由于易于阅读、编写,以及便于机器解析与生成的特性,相比 XML,它更小、更快、更易解析,使得它成为理想的数据交换语言。完全独立于语言的一种文本格式。
JSON 的两种结构:
- “名称/值” 对的集合:不同语言中,它被理解成对象(object)、记录(record)、结构(struct)、字典(dictionary)、哈希表(hash table)、有键列表(keyed list)或者关联数组(associative array)。
- 值的有序列表:大部分语言中,它被理解成数组(array)。
例如用以下 JSON 数据来描述一个人的信息:
{ "name": "Frankie", "age": 20, "skills": ["Java", "JavaScript", "TypeScript"] }
注意,JavaScript 不是 JSON,JSON 也不是 JavaScript。但 JSON 与 JavaScript 是存在渊源的,JSON 的数据格式是从 JavaScript 对象中演变出来的。(从名称上可以体现)
二、JSON 与 JavaScript 的区别
JSON 是一种数据格式,也可以说是一种规范。JSON 是用于跨平台数据交流的,独立于语言和平台。而 JavaScript 对象是一个实例,存在于内存中。JavaScript 对象是没办法传输的,只有在被序列化为 JSON 字符串后才能传输。
JavaScript 类型 | JSON 的不同点 |
对象和数组 | 属性名称必须是双引号括起来的字符串;最后一个属性后不能有逗号 |
数值 | 禁止出现前导零( JSON.stringify() 方法自动忽略前导零,而在 JSON.parse() 方法中将会抛出 SyntaxError);如果有小数点,则后面至少跟着一位数字。 |
字符串 | 只有有限的一些字符可能会被转义;禁止某些控制字符; Unicode 行分隔符 (U+2028)和段分隔符 (U+2029)被允许 ; 字符串必须用双引号括起来。请参考下面的示例,可以看到 JSON.parse() 能够正常解析,但将其当作 JavaScript 解析时会抛出 SyntaxError 错误: |
let code = '"\u2028\u2029"' JSON.parse(code) // 正常 eval(code) // 错误
在 JavaScript 中,我们不能把以下对象叫做 JSON,如:
// 这只是 JS 对象 var people = {} // 这跟 JSON 就更不沾边了,只是 JS 的对象 var people = { name: 'Frankie', age: 20 } // 这跟 JSON 就更不沾边了,只是 JS 的对象 var people = { 'name': 'Frankie', 'age': 20 } // 我们可以把这个称做:JSON 格式的 JS 对象 var people = { "name": "Frankie", "age": 20 } // 我们可以把这个称做:JSON 格式的字符串 var people = '{"name":"Frankie","age":20}' // 稍复杂的 JSON 格式的数组 var peopleArr = [ { "name": "Frankie", "age": 20 }, { "name": "Mandy", "age": 18 } ] // 稍复杂的 JSON 格式字符串 var peopleStr = '[{"name":"Frankie","age":20},{"name":"Mandy","age":18}]'
尽管 JSON 与严格的 JavaScript 对象字面量表示方式很相似,如果将 JavaScript 对象属性加上双引号就理解成 JSON 是不对的,它只是符合 JSON 的语法规则而已。JSON 与 JavaScript 对象本质上是完全不同的两个东西,就像“斑马”和“斑马线”一样。
在 JavaScript 中,JSON
对象包含两个方法,用于解析的 JSON.parse()
和转换的 JSON.stringify()
方法。除了这两个方法,JSON
这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。
三、JSON.stringify()
将一个 JavaScript 对象或值转换为 JSON 字符串。
JSON.stringify(value, replacer, space)
- 参数
value
,是将要序列化成 JSON 字符串的值。 - 参数
replacer
(可选),如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数未提供(或者值为null
),则对象所有的属性都会被序列化。
const people = { name: 'Frankie', age: 20 } const peopleStr1 = JSON.stringify(people, ['name']) const peopleStr2 = JSON.stringify(people, (key, value) => { if (typeof value === 'string') { return undefined } return value }) console.log(peopleStr1) // '{"name":"Frankie"}' console.log(peopleStr2) // '{"age":20}'
- 参数
space
(可选),指定缩进用的空白字符串,用于美化输出(pretty-print)。如果参数为数字,它表示有多少个空格,值大于 10 时,输出空格为 10,小于 1 则表示没有空格。如果参数为字符串,该字符串将被将被作为空格。如果参数没有提供(或者值为null
),将没有空格。注意,若使用非空字符串作为参数值,就不能被JSON.parse()
解析了,会抛出SyntaxError
错误。
一般来说,参数
replacer
和space
平常比较少用到。
看示例:
const symbol = Symbol() const func = () => { } const people = { name: 'Frankie', age: 20, birthday: new Date(), sex: undefined, home: null, say: func, [symbol]: 'This is Symbol', skills: ['', undefined, , 'JavaScript', undefined, symbol, func], course: { name: 'English', score: 90 }, prop1: NaN, prop2: Infinity, prop3: new Boolean(true) // or new String('abc') or new Number(10) } const replacer = (key, value) => { // 这里我其实没做什么处理,跟忽略 replacer 参数是一致的。 // 若符合某种条件不被序列化,return undefined 即可。 // 比如 if (typeof value === 'string') return undefined // 也可以通过该函数来看看序列化的执行顺序。 // console.log('key: ', key) // console.log('value: ', value) return value } // 序列化操作 const peopleStr = JSON.stringify(people, replacer) // '{"name":"Frankie","age":20,"birthday":"2021-01-17T10:24:39.333Z","home":null,"skills":["",null,null,"JavaScript",null,null,null],"course":{"name":"English","score":90},"prop1":null,"prop2":null,"prop3":true}' console.log(peopleStr) console.log(JSON.stringify(function(){})) // undefined console.log(JSON.stringify(undefined)) // undefined
结合以上示例,有以下特点:
- 非数组对象的属性不能保证以特定的顺序属性出现在序列化后的字符串中。(示例可能没体现出来)
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。(如
prop3
) undefined
、任意的函数以及symbol
值,在序列化过程中有两种不同的情况。若出现在非数组对象的属性值中,会被忽略;若出现在数组中,会被转换成null
。- 函数、
undefined
被单独转换时,会返回undefined
。 - 所有以 symbol 为属性键的属性都会被完全忽略掉,即便
replacer
参数中强制指定包含了它们。 - Date 日期调用了其内置的
toJSON()
方法将其转换成字符串(同Date.toISOString()
),因此会被当做字符串处理。 NaN
和Infinity
格式的数值及null
都会被当做null
。- 其他类型的对象,包括
Map
、Set
、WeakMap
、WeakSet
,仅会序列化可枚举的属性。 - 转换值如果含有
toJSON()
方法,该方法定义什么值将被序列化。 - 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
针对最后两点举例说明:
- 若对象本身实现了
toJSON()
方法,那么调用JSON.stringify()
方法时,JSON.stringify()
会将这个对象的toJSON()
方法的返回值作为参数去进行序列化。
const people = { name: 'Frankie', age: 20, toJSON: () => { return { name: 'Mandy' } } } console.log(JSON.stringify(people)) // 结果是 {"name":"Mandy"},而不是 {"name":"Frankie","age":20} // 需要注意的是,若对象的 toJSON 属性值不是函数的话,仍然是该对象作为参数进行序列化。 // 上面还提到 Date 对象本身内置了 toJSON() 方法,所以以下返回结果是: // "2021-01-17T09:40:08.302Z" console.log(JSON.stringify(new Date())) // 假如我去修改 Date 原型上的 toJSON 方法,结果会怎样呢? Date.prototype.toJSON = function () { return '被改写了' } console.log(JSON.stringify(new Date())) // "被改写了"
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
const foo = {} const bar = { b: foo } foo.a = bar console.log(foo) // 如果这时候对 foo 进行序列化操作,就会抛出错误。 JSON.stringify(foo) // Uncaught TypeError: Converting circular structure to JSON
foo 对象和 bar 对象会无限相互引用,可以看下 foo 打印结果如下,如果此时对 foo 进行序列化操作,就会抛出错误:Uncaught TypeError: Converting circular structure to JSON
。
针对这个问题,看看别人的解决方法,看这里 JSON-js。具体用法是,先引入其中的 cycle.js
脚本,然后 JSON.stringify(JSON.decycle(foo))
就 OK 了。
JSON.stringify() 总结:
- 若被序列化的对象,存在
toJSON()
方法,真正被序列化的其实是toJSON()
方法的返回值。 - 若提供了
replacer
参数,应用这个函数过滤器,传入的函数过滤器的值是第 1 步返回的值。 - 对第 2 步返回的每个值,进行相应的序列化。
- 如果提供了
space
参数,执行相应的格式化操作。
四、JSON.parse()
JSON.parse()
方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换(操作)。
JSON.parse(text, reviver)
- 参数
text
,要被解析成 JavaScript 值的字符串。 - 参数
reviver
(可选)转换器,如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。
如果指定了reviver
函数,则解析出的 JavaScript 值(解析值)会经过一次转换后才将被最终返回(返回值)。更具体点讲就是:解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身)分别的去调用reviver
函数,在调用过程中,当前属性所属的对象会作为this
值,当前属性名和属性值会分别作为第一个和第二个参数传入reviver
中。如果reviver
返回undefined
,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。
当遍历到最顶层的值(解析值)时,传入reviver
函数的参数会是空字符串""
(因为此时已经没有真正的属性)和当前的解析值(有可能已经被修改过了),当前的this
值会是{"": 修改过的解析值}
,在编写reviver
函数时,要注意到这个特例。(这个函数的遍历顺序依照:从最内层开始,按照层级顺序,依次向外遍历)
// JSON 字符串 const peopleStr = '{"name":"Frankie","age":20,"birthday":"2021-01-17T10:24:39.333Z","home":null,"skills":["",null,null,"JavaScript",null,null,null],"course":{"name":"English","score":90},"prop1":null,"prop2":null,"prop3":true}' // 若需输出 this 对象,不能使用箭头函数 const reviver = (key, value) => { // 可以通过该函数来看看序列化的执行顺序。 // console.log('key: ', key) // console.log('value: ', value) // 若删除某个属性,return undefined 即可。 // 比如 if (typeof value === 'string') return undefined // 如果到了最顶层,则直接返回属性值 if (key === '') return value // 数值类型,将属性值变为原来的 2 倍 if (typeof value === 'number') return value * 2 // 其他的原样解析 return value } const parseObj = JSON.parse(peopleStr, reviver) console.log(parseObj)
解析结果如下:
{ "age": 40, "birthday": "2021-02-14T06:02:18.491Z", "course": { "name": "English", "score": 180 }, "home": null, "name": "Frankie", "prop1": null, "prop2": null, "prop3": true, "skills": ["", null, null, "JavaScript", null, null, null] }
五、其他
针对 Line separator 和 Paragraph separator 的处理,可以看这里:JSON.stringify 用作 JavaScript
六、拓展
根据 ECMA-262 标准定义,一个字符串可以包含任何东西,只要它不是一个引号,一个反斜线或者一个行终止符。
以下被认为是行终止符:
\u000A
- Line Feed\u000D
- Carriage Return\u2028
- Line separator\u2029
- Paragraph separator