$ 什么是JSON ?
JSON: JS 对象表示法,它是一种数据格式,不是一种编程语言。JSON不支持变量,函数或对象实例,他就是一种表示结构化数据的格式,虽与JS中表示数据的某些语法相同,但它并不局限于JS范畴,JSON并不从属于JS,很多语言都有针对JSON的解析器和序列化器。
$ 用JSON 表示值:
JSON的语法可以表示以下三种类型的值
-
简单值: 字符串,数值,布尔值,
null
- 对象: 对象是一种复杂的数据类型,表示一组无序的键值对,键和值可以是简单值
-
数组: 数组也是一种复杂的数据类型,表示一组有序的值的列表
@ JOSN表示简单值
JSON更多的表示复杂的数据额类型,而简单值只是整个数据结构中的一部分
5 // JSON表示数值5的方式
"Hello world" // JSON表示字符串的方式
【码农注释】JS字符串与JSON表示字符串最大区别在于,后者必须使用双引号,单引号会导致语法错误。
@ JSON表示对象
JSON表示对象是我们最常用的JSON数据格式之一,JSON中的对象与JS中的对象还是有些不同。
- 没有声明变量(JSON中就没有变量的概念)
- 末尾没有分号(JS对象不是JS语句,只是数据表示法,不需要分号)
- 对象的属性必须加双引号 (!真是个常见的错误)
// JS 字面量对象
var person = {
name: "Nicholas",
age: 29
};
// JSON表示对象的方式
{
"name": "Nicholas",
"age": 29
}
【码农注释】JSON对象键值对本质上是用JSON简单值表示的,简单值规则中就需要加双引号,因此这里也要加。当然JSON对象属性值也可以用JSON对象表示,就是多层嵌套而已,道理相同。
@ JSON表示数组
JSON数组的数据表示这更加灵活,它与JS数组的区别和JSON对象上的区别类似不赘述,值得注意的是数组中有对象数组这种数据结构,即[{}, {}, {}]
形式
// JS数组
var values = [25, "Hi", true];
// JSON数组
[25, "Hi", true]
// 更复杂一些的JSON数组
[
{
"title": "JavaScriot",
"author": ["Nicholas"],
"edition": 3
},
{
"title": "Ajax",
"author": ["Nicholas", "Joe", "Jeremy"],
"edition": 2
},
]
$ JSON对象 解析与序列化
JSON流行的原因,并不止因为它类似JS的语法,更重要的是因为可以把JSON数据结构解析为有用的JS对象。早期的JSON解析器基本上就是使用JS的eval()
函数。在ES5的规范中为避免eval()
执行一些恶意代码,改用使用shim
.
JSON对象有两个方法:stringify()
用于将JS对象序列化为JSON字符串,parse()
用于把JSON字符串解析为原生JS值
(1)序列化:JSON.stringify()方法
JSON.stringify()
方法是将一个JS值(对象或者数组)序列化为一个 JSON字符串,值得注意的是,序列化JS对象时,所有函数及原型成员都会被有意忽略,所有值为undefined
的也将被忽略,最终都是值为有效的JSON数据类型的实例属性。
var book = {
title: 'JavaScript',
authors: ["Nichoas C. Zakas"],
edition: 3,
year: 2011,
master: undefined,
lastRead: function() {
return "20180812"
}
};
var jsonBook = JSON.stringify(book);
console.log(jsonBook); // 见 码农注释
console.log(typeof(jsonBook)); // string - 这里指JSON字符串
【码农注释】JSON.stringify()
方法将名为book
的JS对象序列化成了JSON字符串,并将该字符串存放在变量jsonBook
上。jsonBook
打印结果为{"title":"JavaScript","authors":["Nichoas C. Zakas"],"edition":3,"year":2011}
可以看到键值对都加了双引号。注意jsonBook
中并没有属性lastRead
和master
的序列化值,因为它们被故意忽略了。
@ JSON.stringify()序列化选项(参数)
stringify()
函数第一个参数是JS对象我们已经清楚,它还能接受另外两个参数,给要序列化的JS对象增加序列化方式。第一个参数是过滤器,第二个参数是 字符串缩进。
1. 增加过滤器(替换:replacer)
可增加两种过滤器
(1)数组形式,包含想序列化的属性名。未出现的直接过滤。
(2)过滤函数。根据函数规则确定结果,未出现的不过滤,显示原值。
// 增加数组过滤器。数字的值为需要序列化的属性名
var book = {
title: 'JavaScript',
authors: ["Nichoas C. Zakas"],
edition: 3,
year: 2011
};
var jsonBook = JSON.stringify(book, ['edition','title']);
console.log(jsonBook) // {"edition":3,"title":"JavaScript"}
执行结果: {"edition":3,"title":"JavaScript"}
。
【码农解释】序列化结果的属性顺序与数组过滤器的顺序相同,该例中title
在edition
后出现
// 过滤器为函数
var book = {
title: 'JavaScript',
authors: ["Nichoas C. Zakas"],
edition: 3,
year: 2011,
nothing: undefined,
lastRead: function(){
return "20180814"
}
};
// 函数不能取到外边定义
var jsonBook = JSON.stringify(book, function(key, value){
switch (key) {
case "authors":
return value.join(",");
case "year":
return 2018;
default:
return value;
}
});
console.log(jsonBook)
// {"title":"JavaScript","authors":"Nichoas C. Zakas","edition":3,"year":2018}
【码农解释】函数过滤器一定要提供default
项,以便其他值都能正常出现在结果中,(其实不加也能保留其他的属性,但可能存在兼容问题)。实际上,第一次调用这个函数过滤器,传入的键是一个空字符串,而值就是book
对象。
2.增加字符串缩进
控制结果中的缩进,将堆放的字符串数据改用具有格式的字符串格式显示。缩进办法有两种:
(1)数值,表示缩进空格数。
(2)字符串,用字符串填充缩进。
var book = {
title: 'JavaScript',
authors: ["Nichoas C. Zakas"],
edition: 3,
year: 2011,
nothing: undefined,
lastRead: function(){
return "20180814"
}
};
var jsonBook = JSON.stringify(book, null, 4);
console.log(jsonBook)
【注】执行结果为字符串,只不过因为缩进显示成JSON对象如下(如果第三个参数是字符串,则缩进的填充字符为字符串)
{
"title": "JavaScript",
"authors": [
"Nichoas C. Zakas"
],
"edition": 3,
"year": 2011
}
3. toJSON(): 序列化的另一个办法
有时候,JSON.stringify()
还是不能满足对某些对象进行自定义序列化的需求,这种情况下我们可以给对象定义toJSON()
方法,他表示在执行JSON.stringify
时的执行结果。原生Date
也有个toJSON()
方法,将在下一部分说明。
var book = {
title: 'JavaScript',
authors: ["Nichoas C. Zakas"],
edition: 3,
year: 2011,
nothing: undefined,
toJSON: function() {
return this.title
}
};
var jsonBook = JSON.stringify(book);
console.log(jsonBook); // "JavaScript"
关于不同序列化办法执行的优先级
(1)如果存在toJSON()方法,而且能通过他获取有效的值,这调用该方法,否则返回对象本身
(2)如果提供了第二个参数,应用这个函数过滤器,传入函数过滤器的值是第(1)步返回的值
(3)对第(2)步的值进行相应的序列化
(4)如果提供了第三个参数,执行相应的格式化
var book = {
title: 'JavaScript',
authors: ["Nichoas C. Zakas"],
edition: 3,
year: 2011,
nothing: undefined,
toJSON: function(){
return this.title;
}
};
var jsonBook = JSON.stringify(book, function(key, value) {
switch (key) {
case "title":
return 'Ajax';
default:
return value;
}
}, 4);
console.log(jsonBook);
// "JavaScript" 因为优先执行了toJSON(),没有执行第二个参数
(2)解析: JSON.parse() 方法
JSON.parse()
是将JSON字符串解析为原生JS值的函数,在上述代码前提下,继续执行以下代码
var bookCopy = JSON.parse(jsonBook);
最终得到的是以下结果
var bookCopy = {
title: 'JavaScript',
authors: ["Nichoas C. Zakas"],
edition: 3,
year: 2011
}
【码农解释】为什么数据发生了变化?虽然JSON.parse()
与JSON.stringify()
在大多数情况下都是相反的过程,但当原始数据存在JSON.stringify()
主动忽略的属性时,得到的JSON字符串中已经不存在这部分属性,因此执行parse()
得到的原生JS与原数据不同。
注意对比以下情形
var bookCopy = book;
此时得到的bookCopy
的结果与原数据一致。相信很多人能明白这一点,因为JS对象是应用类型,这种方式赋值只是复制了一份原数据的引用到bookCopy
上,也就是说,修改book
数据的同时也会影响到bookCopy
上的值。
但由JSON.stringify()
序列化和JSON.parse()
解析后,得到的bookCopy
与book
是没有任何关系的两个变量,他们的值有可能相同,有可能不同,这取决于原数据是否含有主动忽略的属性。
【注】JOSN.parse()
不允许用逗号作为结尾
// both throw a SyntaxError
JSON.parse("[1, 2, 3, 4, ]");
JSON.parse('{"foo" : 1, }');
1. 解析选项:
JSON.parse()
也可以接受另一个参数,该参数是个函数,将在每个键值对上调用,为区别序列化的第二个参数,该函数被称为还原函数(reviver)。但实际上他们的签名是相同的,接受两个参数,key
和value
,而且都需要返回一个值。
需要注意的是,如果还原函数返回undefined
,则表示要从结果中删除相应的键,如果返回其他值,则将该值插入到结果中。
$ JSON的几个使用场景
JSON格式的数据在一些场景的使用中,如果直接传输会出现异常,此时就需要我们先对数据做一次序列化,否则很可能在访问的时候出现[object,object]
(1) JSON 在 localStorage中的使用
一些时候,你想存储用户创建的一个对象,并且,即使在浏览器被关闭后仍能恢复该对象。
var session = {
'screens' : [],
'state' : true
};
session.screens.push({"name":"screenA", "width":450, "height":250});
session.screens.push({"name":"screenB", "width":650, "height":350});
session.screens.push({"name":"screenC", "width":750, "height":120});
// setItem前要先使用 JSON.stringify 转换为 JSON 字符串
localStorage.setItem('session', JSON.stringify(session));
// 然后通过JSON.parse()取数据
var restoredSession = JSON.parse(localStorage.getItem('session'));
(2) Date.prototype.toJSON()
toJSON()
方法返回Date
对象的字符串形式。
var event = new Date('August 19, 1975 23:15:30 UTC');
var jsonDate = event.toJSON();
console.log(jsonDate);
// expected output: 1975-08-19T23:15:30.000Z
console.log(new Date(jsonDate).toUTCString());
// expected output: Tue, 19 Aug 1975 23:15:30 GMT
$ JSON的深拷贝【难点】
先明白两个概念: 什么是深拷贝(深复制),什么是浅拷贝(浅复制)?对于值类型,深拷贝与浅拷贝没什么区别,关键是对于引用类型的数据结构
浅复制(Shallow Copy) —- 对于引用类型,只复制了指向堆内存中的地址引用,并没有开辟新的栈空间。对于浅复制的两个变量,任何一个的值发生了改变,另一个也会随着改变。
深复制(Deep Copy) —- 开辟了一个新的栈空间用来创建一个原始字段的内容相同的对象。拷贝前后的两个变量完全相互独立,修改其中一个对象的属性值不会影响另一个对象的属性值。
@ 深拷贝的实现办法
1. 通过递归问解析解决 (推荐)
//深复制,要想达到深复制就需要用递归
function deepCopy(o,c){
var c = c || {}
for (var i in o) {
if (typeof o[i] === 'object') {
//要考虑深复制问题了
if (o[i].constructor === Array) {
//这是数组
c[i] =[]
} else {
//这是对象
c[i] = {}
}
deepCopy(o[i],c[i])
} else {
c[i] = o[i]
}
}
return c
}
// 测试代码
var result = {name:'result'}
var test= {
nation : '中国',
skincolr :'yellow',
deep: {
childDeep: {
names: ['judy', 'cici']
}
},
func: function() {
return "func return";
}
}
result = deepCopy(test, result)
console.dir(result)
- 通过JSON的解析解决 (只能使用在不存在被主动忽略属性的对象中)
var test= {
nation : '中国',
skincolr :'yellow',
deep: {
childDeep: {
names: ['judy', 'cici']
}
}
}
var result = JSON.parse(JSON.stringify(test));
console.log(result);