关于JSON,看这一篇文章就够了

1,128 阅读8分钟

前言

XML曾经一度成为互联网传输数据的事实标准。第一代Web服务很大程度是以XML为基础。但是,有人认为XML过于冗杂和啰嗦。

2006年,Douglas Crockford 在国际互联网工程任务组(IETF)指定了JavaScript简谱(JSON, JavaScript Object Notation)标准,即RFC 4627。但实际上,JSON在2001年就开始使用了。JSON是JavaScript的严格子集,利用JavaScript中的几种模式来表示结构化数据。Crockford将JSON作为XML替代的一个方案提出,因为JSON可以直接传给eval()而不需要创建DOM。

理解JSON最关键的一点是要把它当成一种数据格式,而不是变成语言。JSON不属于JavaScript,它们只是拥有相同的语法而已。JSON也不是只能在JS中使用,它是一种通用数据格式。很多数据都有解析和序列号JSON的内置能力。

理解JSON语法

JSON语法支持三种类型的值。

  1. 简单值:字符串、数值、布尔值和null。不支持undefined
  2. 对象:有序键/值对,键是字符串,值可以是1,2,3中的任何一种。
  3. 数组:通过数值索引访问的有序列表,值可以是1,2,3中的任何一种

总结就是键只能是字符串,值可以是JS中的七种类型中除去 undefinedsymbol。对象中的最后一个键/值对不能有逗号。

简单值

5,"wuzequn"这种都是简单值,与JS的差别是JSON中的字符串必须用双引号包起来,无论是作为属性还是作为值。

对象

JSON与JS对象的主要差别是:

  1. 属性必须用双引号包起来
  2. 最后一组键值对不能带逗号
  3. 没有分号

数组

语法规则在除了前两者的区别外基本和JS一致

解析JSON

JSON的迅速流行不仅仅因为其语法与JS类似,很大程度还因为JSON可以被直接解析成可用的JS对象。与解析为DOM的XML相比,这个优势可以说是碾压。为此,JS开发者可以很方便的使用JSON数据。因此,JSON出现后就迅速成为了Web服务的事实序列化标准。

JSON对象

早期的JSON解析器基本上相当于JS的eval函数。因为JSON是JS语法的子集,所以eval()可以解析、解释,并将其作为对象或数组返回。ES5新增了JSON全局对象,正式引入解析JSON的能力。这个对象在所有主流浏览器包括Node中都得到了支持。其兼容性如下:

image-20211213191308977.png JSON对象有两个方法:stringify()parse(),这两个方法分别将JS序列化成JSON字符串,以及将JSON解析为原生JS值。例如:

const t = {
    a: 1,
    b: 2,
    c: '3'
}
console.log(JSON.stringify(t));

结果为:

{"a":1,"b":2,"c":"3"}

如果将序列化的结果作为参数传入parse(),可以得到一个原对象的深克隆。

注意这种深克隆被网友戏称为破产版的深克隆,因为其有如下两个致命缺点:

  1. 如果存在循环引用将会抛出异常
  2. 不支持正则、函数等复杂数据类型

序列化选项

JSON.stringify()方法除了接收要序列化的对象,还可以接受两个参数。这两个参数可以用于指定序列化JS对象的方式。第一个参数是过滤器,类型可以是数组或函数,第二个参数用于控制结果的缩进。如下:

image-20211213192645722.png

过滤器

对于如下代码

const book = {
    title: 'NB',
    edition: 4,
    year: 2021,
    authors: ['wzq','rock']
}

console.log(JSON.stringify(book,(key,val)=>{
    switch(key) {
        case 'authors':
            return val.join(',')
        case 'year':
            return 5000
        case 'edition':
            return undefined
        default:
            return val
        
    }
}));

输出结果为:

{"title":"NB","year":5000,"authors":"wzq,rock"}

从上面的结果可以看到,值为undefined的属性直接被删掉了,这也从另一个维度解释了为什么JS对于对象不存在的属性输出结果为undefined

缩进

JSON.stringify()的第三个参数可以控制缩进和空格,这也是为什么经常能看到这种代码:

JSON.stringify(a,null,4);

的原因。

其实第三个参数除了是数字(表示几个空格),还可以是字符串,表示用这个字符串缩进,如:

const book = {
    title: 'NB',
    edition: 4,
    year: 2021,
    authors: ['wzq', 'rock']
};

console.log(
    JSON.stringify(
        book,
        (key, val) => {
            switch (key) {
                case 'authors':
                    return val.join(',');
                case 'year':
                    return 5000;
                case 'edition':
                    return undefined;
                default:
                    return val;
            }
        },
        '----'
    )
);

输出:

{
----"title": "NB",
----"year": 5000,
----"authors": "wzq,rock"
}

注意这个字符串的长度不能超过10,超出部分将被截断,如果传入的数字大于10,则会自动设置为10。

toJSON

toJSON方法可以用来自定义JSON序列化的方式。经典的例子就是Date对象的toJSON方法,可以将JS的Date对象转换为ISO 8601日期字符串(本质上与调用toIOSString()一致)

const book = {
    title: 'NB',
    edition: 4,
    year: 2021,
    authors: ['wzq', 'rock'],
    toJSON() {
        return this.title;
    }
};

如果对book对象序列化,得到的是字符串NB

toJSON()方法可以返回任意序列化值,都可以起到相应的作用如果对象被嵌在另一个对象中,则undefined会变为null

toJSON()方法可以与过滤函数一起使用,其二者的执行顺序如下:

  1. 如果可以获取实际的值,则调用toJSON方法获取实际的值,否则使用默认的序列化。
  2. 如果提供了第二个参数,则应用过滤。传入过滤函数的值就是第1步返回的值。
  3. 第2步返回的每个值都会进行相应地进行序列化。
  4. 如果提供了第三个参数,则相应地进行缩进

看如下代码:

const p = {
    r: 2,
    q: {
        r: 1,
        toJSON: () => {
            return this.r;
        }
    }
};

console.log(
    JSON.stringify(
        p,
        (key, val) => {
            switch (key) {
                case 'r':
                    return 'r';
                default:
                    return val;
            }
        },
        4
    )
);

输出结果:

{
    "r": "r"
}

JSON.stringify 使用的6个tips

为了方便查看JSON.stringify的输出结果,这里封装了一个stringify函数

/**
 * @description 打印JSON.stringify后的对象
 * @param {any} obj 
 */
function stringify(obj) {
    console.log(JSON.stringify(obj));
}

1、数组中的undefined、函数、symbol值会被转化成null,对象中的会被忽略,直接转化会成undefined

stringify({a: undefined, b: 1}); // {"b":1}
stringify({a: () => {}}); // {}
stringify({a: Symbol(1)}); // {}
stringify({[Symbol(1)]: 1}); // {}
stringify([undefined, () => {}, Symbol(1)]); // [null,null,null]
stringify(() => {}); // undefined
stringify(Symbol(1)); // undefined
stringify(undefined); // undefined

2、boolean、number、string 的包装对象会自动转换成原始值

stringify([new Boolean(1), new Number(1), new String(1)]); // [true,1,"1"]
stringify({a: new String(1), b: new Boolean(1), c: new Number('1')}); // {"a":"1","b":true,"c":1}

3、NaN Infinity null 都会被当成null

stringify([null, NaN, Infinity]); // [null,null,null]
stringify(NaN); // null
stringify(Infinity); // null
stringify({a: null, b: NaN, c: Infinity}); // {"a":null,"b":null,"c":null}

4、Date对象会自动被转化成字符串

stringify(new Date()); //"2021-12-23T08:56:38.396Z"
stringify([new Date()]); // ["2021-12-23T09:42:16.369Z"]
stringify({a: new Date()}); // {"a":"2021-12-23T09:42:16.369Z"}

5、Set,Map 对象会变成{}

stringify(new Set([1, 2, 3])); // {}
const map = new Map();
map.set('stringify', 1);
stringify(map); // {}
stringify([map]); // [{}]
stringify({a: map}); //{"a":{}}

6、BigInt对象转换报错

stringify(BigInt(9007199254740991)); // Do not know how to serialize a BigInt

解析选项

JSON.parse()也可以接受一个额外的参数receiver,类型为函数,这个函数会对每个键值对都调用一次。

image-20211214102321123.png receiver函数如果返回undefined,则结果会把相应的键删除。如果返回其他任何值,则该值就会成为相应键的值插入到结果中。receiver经常用于把日期字符串转换为Date对象。如:

const day = {
    week: 2,
    now: "2021-12-14"
}
const dayString = JSON.stringify(day)
console.log(dayString);
const dayCopy = JSON.parse(dayString,(key,val)=> key === 'now' ? new Date(val) : val);
console.log(dayCopy.now.getFullYear());

输出:

{"week":2,"now":"2021-12-14"}
2021

tips

  • JSON.parse 加上try catch比较保险

JSON5

JSON for Humans

这是引用自json5.org的一句话,意译过来就是更人性化的json

特性总结

JSON5支持了以下JSON不支持的ECMAScript5.1特性。

对象

  • 对象的键可以是任何ES 5.1 的合法标志符
  • 对象可以有尾逗号

数组

  • 数组可以有尾逗号

字符串

  • 字符串可以是单引号包裹
  • 可以通过换行符支持多行字符串
  • 字符串可以包含转义字符

数字

  • 数字可以是16进制
  • 数字可以有前导小数点和尾部小数点
  • 数字可以是 IEEE 754的正无穷(Infinity)负无穷(-Infinity)以及NaN
  • 数字可以以+开头

注释

  • 单行注释和多行注释都支持

空格

  • 支持多余的空格

示例

{
  // comments
  unquoted: 'and you can quote me on that',
  singleQuotes: 'I can use "double quotes" here',
  lineBreaks: "Look, Mom! \
No \\n's!",
  hexadecimal: 0xdecaf,
  leadingDecimalPoint: .8675309, andTrailing: 8675309.,
  positiveSign: +1,
  trailingComma: 'in objects', andIn: ['arrays',],
  "backwardsCompatible": "with JSON",
}

更多细节请自行查看官方文档

总结

  1. JSON的语法是JS的子集,值不能为undefined,可以是字符串、数值、布尔值、对象和null,键只能为带引号的字符串。
  2. JSON.stringify()可以接受三个参数,第一个是要被序列化的JS对象;第二个是一个函数replacer,其参数为key和value,可以指定key返回的value;第三个是缩进格式,数字代表空格数量,字符串则用相应的字符串填充缩进,注意缩进长度不能超过10。
  3. 如果想自定义对象的序列化结果可以在该对象上添加toJSON()方法,序列化时会基于这个方法返回适当的JSON表示。
  4. JSON.parse()可以接收两个参数,第一个参数是被序列化的JSON字符串,第二个参数是receiver函数,参数也是key和value。receiver函数如果返回undefined则该键会被删除,返回其他的会被作为对应键的值插入到结果中。
  5. JSON5比JSON更加灵活,是JSON的超集,支持很多ES5.1的语法