JSON.stringify()

274 阅读7分钟

前言

JSON(JavaScript Object Notation)是一种轻量级数据交换格式。

JSON对象有两个方法:

  • JSON.stringify(),将 JavaScript 对象/值转换为 JSON 字符串。
  • JSON.parse(),将 JSON 字符串解析成原生 JavaScript 对象/值。

这里主要讨论 JSON.stringify() 方法。

特性

1. 转换值如果有toJSON()方法,该方法定义什么值将被序列化

如果被序列化对象含有toJSON()方法,那么toJSON()方法会覆盖默认的序列化行为,被序列化的值将不再是原来的值,而是toJSON()方法的返回值。

toJSON() 方法用于更精准的控制序列化,可以看做是对stringify函数的补充。

    //通过toJSON()方法在姓名后加上同学,年龄乘以2并返回
    var jsonObj = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
        toJSON: function () {
            return { name: this.name + '同学', age: this.age * 2 };
        },
    };

    console.log(JSON.stringify(jsonObj));//{"name":"zhangsan同学","age":40}

那么假如 toJSON() 方法是箭头函数呢?

    //通过toJSON()方法在姓名后加上同学,年龄乘以2并返回
    var jsonObj = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
        toJSON: () => {
            return { name: this.name + '同学', age: this.age * 2 };
        },
    };

    console.log(JSON.stringify(jsonObj));//{"name":"同学","age":null}

这个是因为this指向的window,而不是jsonObj,在全局作用域下并不存在name和age变量。

2. undefined、任意的函数以及 symbol 值在序列化的表现

这其中又分为三种情况:

2.1 作为单独的值被序列化

    console.log(JSON.stringify(undefined)); //undefined
    console.log(JSON.stringify(function test() {})); //undefined
    console.log(JSON.stringify(Symbol('hello world'))); //undefined

undefined、任意的函数以及 symbol 值作为单独的值被序列化会返回undefined。

2.2 作为对象属性的值被序列化

    var jsonObj2 = {
        name: 'zhangsan',
        address: undefined,
        say: function () {
                console.log('hello world');
        },
        work: Symbol('programmer'),
    };
    console.log(JSON.stringify(jsonObj2)); //{"name":"zhangsan"}

undefined、任意的函数以及 symbol 值作为对象属性值被序列化时会被忽略。

为什么axios发送请求时,如果req的body包含 undefined 值的参数,在发给服务端的请求中会消失?

就是因为作为对象属性的值被序列化时被忽略了。buildURL.js 37~56行

2.3 作为数组元素被序列化

    var arr = [
        undefined,
        function test() {
            console.log('hello world');
        },
        Symbol('programmer'),
    ];
    console.log(JSON.stringify(arr));//[null,null,null]

undefined、任意的函数以及 symbol 值作为数组元素被序列化会返回null。

为什么有些属性无法被 stringify 呢?

因为 JSON 是一个通用的文本格式,和语言无关。设想如果将函数定义也 stringify 的话,如何判断是哪种语言,并且通过合适的方式将其呈现出来将会变得特别复杂。特别是和语言相关的一些特性,比如 JavaScript 中的 Symbol。

ECMASCript 官方也特意强调了这一点:

It does not attempt to impose ECMAScript’s internal data representations on other programming languages. Instead, it shares a small subset of ECMAScript’s textual representations with all other programming languages.

3. 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中

正如上面所说,JSON.stringify() 序列化时会忽略一些值,所以不能保证序列后的字符串还是之前的顺序,数组除外。

    var jsonObj3 = {
        name: 'zhangsan',
        address: undefined,
        say: function () {
                console.log('hello world');
        },
        work: Symbol('programmer'),
        interests: ['book', 'coding'],
    };
    console.log(JSON.stringify(jsonObj3)); //{"name":"zhangsan","interests":["book","coding"]}

    console.log(
        JSON.stringify(['zs',undefined,Object,Symbol('hello'),'work',])
    ); //["zs",null,null,null,"work"]

4. 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值

    console.log(
        JSON.stringify([
            new Number(1),
            new String('false'),
            new Boolean(false),
        ])
    );// '[1,"false",false]'

包装对象

所谓的包装对象,指的是与数值、字符串、布尔值分别相对应的 NumberStringBoolean 三个原生对象。这三个原生对象可以把原始类型的值包装成对象

let a1 = new Number(1111);
let a2 = new String('hello world');
let a3 = new Boolean(true);

console.log(typeof a1); // "object"
console.log(typeof a2); // "object"
console.log(typeof a3); // "object"

console.log(a1 === 1111); // false
console.log(a2 === 'hello world'); // false
console.log(a3 === true); // false

5. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误

    let obj = { name: 'zhangsan' };
    let obj2 = { originObj: obj };
    obj.newObj = obj2;
    console.log(JSON.stringify(obj));
    // Uncaught TypeError: Converting circular structure to JSON
    // --> starting at object with constructor 'Object'
    // |     property 'newObj' -> object with constructor 'Object'
    // --- property 'originObj' closes the circle
    // at JSON.stringify (<anonymous>)

6. 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们

console.log(
    JSON.stringify({ [Symbol.for('foo')]: 'foo' }, function (k, v) {
        if (typeof k === 'symbol') {
            return 'a symbol';
        }
    })
); //undefined

关于 replacerJSON.stringify() 的第二个参数,下面将会仔细介绍。

7. Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理

JSON.stringify({ now: new Date() });
// "{"now":"2019-12-08T07:42:11.973Z"}"

8. NaNInfinity 格式的数值及 null 都会被当做 null

无论是作为单独的值,数组元素,还是对象属性的值,被序列化时都会返回 null。

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

9. 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性

    // 不可枚举的属性默认会被忽略:
    console.log(
        JSON.stringify(
            Object.create(null, {
                x: { value: 'x', enumerable: false },
                y: { value: 'y', enumerable: true },
            })
        )
    );//{"y":"y"}

语法

JSON.stringify(value[, replacer [, space]])

value

被序列化的 JSON 对象/值。

replacer 可选

  • 如果该参数为null或者没有提供,则对象私有的属性都会被序列化。 (默认选项)

  • 如果该参数是一个函数,则在被序列化过程中,被序列化的每个属性都会经过该函数的转换和处理,类似于 mapfilter 中的callback

    举个 🌰

       
        var jsonObj2 = {
            name: 'zhangsan',
            address: undefined,
            say: function () {
                console.log('hello world');
            },
            work: Symbol('programmer'),
        };
        console.log(JSON.stringify(jsonObj2)); //{"name":"zhangsan"}
         //按照之前介绍的特性,应该打印出{"name":"zhangsan"},但是经过 replacer 函数的处理,所返回的值就完全不一样了。
        console.log(
            JSON.stringify(jsonObj2, (key, value) => {
                if (value === undefined) {
                    return 'undefined';
                } else if (typeof value === 'function') {
                    return value.toString();
                } else if (typeof value === 'symbol') {
                    return value.toString();
                }
                return value;
            })
        ); 
        //{"name":"zhangsan","address":"undefined","say":"function () {\n\t\t\t\tconsole.log('hello world');\n\t\t\t}","work":"Symbol(programmer)"}
    
    
  • 如果该参数是一个数组,只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中,类似于 loadshpick

    举个 🌰

        var jsonObj4 = {
            name: 'zhangsan',
            address: 'shanghai',
            age: 20,
            interests: ['book', 'coding'],
        };
        console.log(JSON.stringify(jsonObj4)); //{"name":"zhangsan","address":"shanghai","age":20,"interests":["book","coding"]}
        
        //按照之前介绍的特性,会全部都被序列化,但当我只想name和address被序列化,该怎么办?
        console.log(JSON.stringify(jsonObj4, ['name', 'address'])); //{"name":"zhangsan","address":"shanghai"}
    
    

space 可选

指定缩进用的空白字符串,用于美化输出。

  • 如果该参数没有提供(或者为null),将没有空格。 (默认选项)

  • 如果参数是数字,它代表有多少个的空格,最多10个,如果小于1,就意味着没有空格。

    也就是说,被序列化后每一级别会比上一级别多缩进这个数值的空格。

    依旧来举个 🌰

    var jsonObj5 = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
    };
    console.log(JSON.stringify(jsonObj5)); //{"name":"zhangsan","address":"shanghai","age":20,"interests":["book","coding"]}
    
    //为了增强可读性,我们采用4个空格缩进
    console.log(JSON.stringify(jsonObj5, null, 4));    
    // {
    //     "name": "zhangsan",
    //     "address": "shanghai",
    //     "age": 20,
    //     "interests": [
    //         "book",
    //         "coding"
    //     ]
    // }

  • 如果参数是字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格。

    每一级别会比上一级别多缩进该字符串(或该字符串的前10个字符)

    举个 🌰

    使用上面的jsonObj5
    //字符串
    console.log(JSON.stringify(jsonObj5, null, '🌰🌰'));
    // {
    // 🌰🌰"name": "zhangsan",
    // 🌰🌰"address": "shanghai",
    // 🌰🌰"age": 20,
    // 🌰🌰"interests": [
    // 🌰🌰🌰🌰"book",
    // 🌰🌰🌰🌰"coding"
    // 🌰🌰]
    // }
    
    //'asdfghjklzxcvbnm'字符串长度超出10了,所以截取10个,也就是'asdfghjklz'
    console.log(JSON.stringify(jsonObj5, null, 'asdfghjklzxcvbnm'));
    // {
    // asdfghjklz"name": "zhangsan",
    // asdfghjklz"address": "shanghai",
    // asdfghjklz"age": 20,
    // asdfghjklz"interests": [
    // asdfghjklzasdfghjklz"book",
    // asdfghjklzasdfghjklz"coding"
    // asdfghjklz]
    // }

序列化和反序列化

把数据结构或者对象转换成某种格式的过程称为「序列化」,而将序列化过程的结果反向转换回某种数据结构或对象的过程称为「反序列化」。

应用场景

1. 判断两个值是否相等

    let obj = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
    };

    let obj1 = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
    };

    console.log(JSON.stringify(obj) === JSON.stringify(obj1));//true

但是这种判断有很大的局限性,对于相同属性相同值但属性顺序不同的两个对象,也是不相等的。

    let obj = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
    };

    let obj1 = {
        name: 'zhangsan',
        age: 20,
        interests: ['book', 'coding'],
        address: 'shanghai',
    };

    console.log(JSON.stringify(obj) === JSON.stringify(obj1));//false

2. 结合 localStorage 使用

localStorage 中的键值对总是以字符串的形式存储,所以需要序列化。

    let obj = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
    };
    
    localStorage.setItem('zhangsanObj', JSON.stringify(obj))
    console.log(localStorage.getItem('zhangsanObj'))//'{"name":"zhangsan","address":"shanghai","age":20,"interests":["book","coding"]}'

3. 深拷贝

使用 JSON.parse() 和 JSON.stringify() 进行深拷贝。

    const obj = {
        name: 'zhangsan',
        address: 'shanghai',
        age: 20,
        interests: ['book', 'coding'],
    };
    
    const obj1 = JSON.parse(JSON.stringify(obj))

这种方法深拷贝非常简单,但是有很大的局限性。

  • 被序列化对象中不能有函数,undefined ,正则,都会被忽略掉。

  • 被序列化对象中的 Date 类型数据会被转化为字符串类型。

  • 循环引用会导致报错。

引用