JSON使用指南

102 阅读11分钟

这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

JSON总览

json是一种数据格式,可以在客户端与服务器端间进行传输。

它是 ECMAScript 的子集(全名为Javascript Object Notation,js对象表示符,它和js是同属一个规范下的),是一种轻量级的资源交换格式。

Java后端配置文件常用的xml也是一种数据交换格式,不过与json相比就太繁琐了,比如我们要表达名字这一属性时,很明显地看出两者的差别:

xml

<person>
    <name>HappyCandy</name>
</person>

json

{
    "name" : "HappyCandy"
}

json与xml的比较,不同点:

(1)可读性:基本相同,XML的可读性比较好;

(3)编码难度:相对而言,JSON的编码比较容易;

(4)解码难度:JSON的解码难度基本为零,XML需要考虑子节点和父节点

(5)数据体积方面:JSON相对于XML数据体积更小,传递的速度比较快;

(6)数据交互方面:JSON与javascript的交互更加方便,更容易解析处理,更好的数据交互(换);

(7)数据描述方面:XML对数据描述性比较好;

(8)传输速度方面:JSON的速度远远快于XML

JSON基本语法

我们只将需要注意的语法讲一讲:

JSON最顶层的几种类型

最顶层的意思就是它在.json文件中是可以单独存在的,比如

123

这在json中是合法的,不是说一定就是我们常见的:

{ 
    "name" : "HappyCandy"
}

最顶层的形式是多样的,就和js类似

①. 基本类型

  1. json只支持字符串类型的双引号形式

  2. json中没有undefined,有null,其他类型基本与js相同

②. 对象类型

  1. 对象中的属性名必须用双引号括起来

  2. 对象的属性值既可以是基本类型也可以是对象类型

  3. json对象类型中是不包含函数这一类型的 ③. 数组

最顶层是数组在.json文件中也是非常common,诸如小程序的app.json中我们就可以看到

  1. xxx.json中不允许注释

  2. json中最后一项不可以加逗号,言外之意,其他项之间要用逗号隔开 以示区分

  3. 它是 ECMAScript 子集的意思是它有的语法,在js中都是有的,但应注意它只是一种数据格式,要支持其他语法如let/const变量定义,或是函数等 都是不可能的

JSON序列化

序列化的需要

某些情况下,我们希望将js中的复杂类型转化成json格式的字符串, 那么类似什么情况才有这样的需求呢,什么时候要进行json的序列化呢?

比如现在我们想要将一个对象存入到localStorage中,那么就要调用localStorage.setItem(String key, String value) 进行存储,但一个js对象若直接放到第二个参数位置,那么若被强制将一个对象转为字符串类型,结果就是 [object Object]这样一个字符串。

若到浏览器的localStorage中查看 / 利用localStorage.getItem(key),结果就是[object Object],这明显是不符合我们需求的,所以这时候 就要用到 JSON.stringify(obj) 将对象obj转成JSON格式的字符串:

const obj = {
    name : "happyCandy", 
    age: 22, 
    friends: {
        name: "JayZhou"
    }, 
    hobbies: ["k歌", "篮球"]
}
const jsonString = JSON.stringify(obj); 
// {"name":"happyCandy","age":22,"friends":{"name":"JayZhou"},"hobbies":["k歌","篮球"]}
localStorage.setItem("obj", jsonString);

倘若我们又想要转化回对象,那么就要使用到下面要讲的JSON.parse(key)方法了:

const objString = localStorage.getItem(key);
const obj = JSON.parse(objString); 

注意全部大写的JSON是浏览器给我们全局提供的,不需要我们自己定义。

基本的用法

JSON.stringify(value) // value为js的对象/值

返回结果

在原值上加上'',由于可能里面已经有"",所以在外面加上单引号''表示其是字符串

// Number类型
console.log(JSON.stringify(1234) === '1234'); // true
console.log(JSON.stringify(1234) === 1234); // false

// String类型
console.log(JSON.stringify(String(1234)) === '1234') // false
console.log(JSON.stringify(1234) === '"1234"'); // true

序列化细节

所谓细节就是在调用JSON.stringify方法时,除了基本用法JSON.stringify(obj)之外的其他可以注意的点: 现在先定义一个js对象;

const obj = {
    name : "happyCandy", 
    age: 22, 
    friends: { // 嵌套的对象
        name: "JayZhou"
    }, 
    hobbies: ["k歌", "篮球"], // 一个数组 
    toJSON : function() { // 特殊的toJSON函数
       return "I'll help you transform obj to json" 
    }
}

下面我们将围绕上面的对象进行几种.stringify(将上面的对象转成JSON字符串)的不同操作:

更多的参数

(一)第二个参数:

  • stringify第二个参数replacer(相当于一个转换器),有两种转换方式:
  1. .stringify只取obj的部分属性转为json字符串
const jsonStrng1 = JSON.stringify(obj, ["name", "friends"]);
console.log(jsonStrng1);
  1. 传入回调函数,对属性值进行操作变换,回调函数是固定好了的,函数中return的value就是我们key所对应的新的值
const jsonString2 = JSON.stringify(obj, (key, value) => {
    if(key === "age") return value + 1; // 将json字符串化后年龄 + 1
    return value;
})
console.log(jsonString2);

关于这个回调函数的处理过程与value返回的处理规则要细说一下:

①. 回调函数是怎么进行每次的传参处理的

  • 第一次调用时,key为空字符串"",value则为需要序列化的整个对象/值
  • 之后处理时,会把上一次的的value传过来从上往下依次分解,属性键给key,属性值给value,如果属性值又是个对象那么继续往里面深入嵌套
  • 整个对象都递归遍历完成后返回。

②. value返回时的取值规则,因为新的value值是根据回调函数return的value值来确定的,对于一些特殊的返回应该怎么处理?

其实下面“你会忽略的细节”中讲,对Symbol,函数,undefined分别单独出现,在对象中出现和在数组中出现都有阐述!

(二)第三个参数space

控制转化后的缩进格式,语法上没有什么大用。默认情况就是空格(这种情况下我们就可以输出数字表示几个空格),我们还可以自己规定"^^^","~~~"等形式。另外,若第二个参数没有什么特殊的指定就直接写null就可以了。

const jsonString3 = JSON.stringify(obj, null, 4),  // 缩进四个空格
    jsonString4 = JSON.stringify(obj, null, "---");  // 缩进三个破折号
console.log(jsonString3, jsonString4);

你会忽略的点

  1. 若obj对象中有toJSON方法,那么JSON.stringify()方法的转化结果就是obj.toJSON()方法的返回值。因为.stringify方法本身就是调用toJSON来进行转化的 !像我们在obj中定义了toJSON方法后,上面的返回结果就都是 "I'll help you transform obj to json"了。 就比如js中的Date,由于Date对象自己就设计有toJSON方法,所以是可以正确地以字符串的形式序列化的!

  2. 在序列化的时候,一些js中有,但json没有的东西该怎么处理?比如对象中的Symbol属性函数undefined怎么处理? 这就取决于序列化的时候它们位于哪里了? 我们先明确一下对象是无序的集合,数组是有序的集合,so:       ①. 它们三个单独被序列化,由于json没有它们这种类型,所以直接undefined

      ②. 对象是无序集合,序列化时只枚举可枚举的,像它们三个定义都没有,只能是跳过了

      ③. 数组是有序的集合,必须一个萝卜一个坑,所以它们三个在序列化后都是被定义为null的

  1. 上面三个是json不允许出现的,那么允许出现但又很特殊的呢?像NaN,Infinity和null呢,它们三个统一被认为是null

序列化的威力

第二参数replacer的起死回生

上面其实我们已经介绍了replacer是如何使用的,这里只是更加深入地介绍一下它的使用场景,比如通过它,可以令我们上面所禁止的undefined,function和symbol作为值在序列化时起死回生:(若Symbol作为键那么还是无能为力的,都枚举不到

const obj = {
    func : function() { return "I am a function" },
    symb: Symbol("aaa"),
    unde: undefined
}
const jsonString = JSON.stringify(obj, (key, value) => { 
    if(typeof value === "undefined") return "undefined"; 
    else if(typeof value === "symbol") return value.toString(); 
    else if(typeof value === "function") return value.toString(); 
    // symbol和function就返回系统设计的toString()方法进行返回
    return value;
})
console.log(jsonString);
// {"func":"function() { return \"I am a function\" }","symbol":"Symbol(aaa)","unde":"undefined"}

】 了解一下系统传入replacer的参数情况:

传入replacer函数的第一组参数 : key为空字符串,value为所有的键值对组成的一个对象,之后就可以解析键值对!

JSON字符串解析parse

上面的JSON.stringify是将 js对象/值 转化为 json格式的字符串,而现在我们要做的就是将json字符串转回为js对象 --- JSON.parse()

parse的参数传入

与stringify相比,parse的参数较为简单,就两个参数:

JSON.parse(text, reviver)

(一)第一个参数text

text中可以传入''包裹物,Number/null/boolean三个可以不加单引号,单引号中放的东西就是stringify中放入的东西,比如:

JSON.parse('')

可能吗?我们stringify后的结果不可能为'',即使是最有希望的:

JSON.stringify(null); // 'null'
JSON.stringify(undefined); // undefined

也无法得出空!

返回值

就是一个js 对象/值

(二)第二个参数reviver

这个reviver和上面stringify的replacer很像,也是将相应的value处理成为新的value,但处理过程却是相逆的:

  • replacer是从外到内,reviver从内往外遍历,最终到达最顶层,解析 对象/值 本身。
  • 当遍历到最顶层时,因为没有属性了,参数key是空字符串'',参数value则是 对象/值 本身,到最外层就返回。

reviver的返回规则

  • 返回undefined,则删除该层属性;
  • 如果返回了其他值,则该值会成为当前属性的新值;
  • 另外要注意replacer和reviver的每个属性都必须有他能对应的返回值的,否则它就丢失了 !

key和value的确定

适用于replacer和reviver,对象中key,value与属性值,属性名对应;数组中key,value和索引,索引对应值对应;基本类型,key为空字符串,value为值本身 !

parse以外的东西

我们现在知道JSON.parse()可以将一个特定格式的字符串转为js对象,那么js中还有其他方式也可以实现 字符串 --> 对象 的转变吗?

方式一:eval动态拼接计算

要想形成对象{}这种形式,必须在外面套一层括号()才行

console.log(typeof eval("({})"));  // object, 就是{}
console.log(typeof eval('({ name : "happyCandy"})')); 
// object,就是 { name: 'happyCandy' }

方式二:new Function方式

这种创建函数的方式可能较为陌生,new Function(arg1 , arg2 ,arg3 ,…, argN , body),最后一个参数是函数体,其余前面的都是形参,并且形式都是字符串。 这种函数创建方式实际上是函数创建方式的本源,其余我们最熟悉的方式都是这种方式的语法糖(如function functionName(){}, const functionName = function() {})。

其实从这种方式我们也可以看出函数实际上是功能完整的对象,另外function foo(){}形式也是和变量一样有变量提升性质的,用变量接收地址的就无法提升了,具体详见 聊聊new Function这个陌生面孔及函数作用域和函数执行顺序

那么现在看下用new function(){}的方式怎么将字符串变成对象

console.log(typeof (new Function("return {}"))() ); // object

JSON的深拷贝应用

综合利用JSON.parse()和JSON.stringify()的综合应用,在将深拷贝之前,我们先了解一下“引用赋值” 和 “浅拷贝”:

现有一对象obj

const obj = {
    name : "happyCandy", 
    age: 22, 
    friends: {
        name: "JayZhou"
    }, 
    hobbies: ["k歌", "篮球"]
}
  1. 引用赋值 两者完全就是同一个对象
const info = obj;
  1. 浅拷贝 是创建了一个自己的空间,但只是进行了第一层的一一赋值,若对象中还有对象,那么那个内嵌的对象就还是和原对象共享的

两种方式:①. Object.assign(obj) ②. 展开运算符

const info1 = Object.assign(obj), 
    info2 = {... obj}; 
    // 展开运算符,将key-value复制了一份,也是浅拷贝(的确是创建了一个新的对象,但若对象中还有的对象,就不行了)
  1. 深拷贝
const jsonString = JSON.stringify(obj);
const info3 = JSON.parse(jsonString);

然后这种深拷贝存在很多问题:

①. 忽略函数,Symbol作为键的属性

③.不允许循环引用(对象中的属性值是自己)

③. 对于set和map等只枚举可枚举的部分

之后我们会讲“手写深拷贝”解决以上问题

参考文献

[1] 你不知道的 JSON.stringify() 的威力

[2] JSON.parse 和 JSON.stringify 详解