深入 JSON.stringify

·  阅读 1368
深入 JSON.stringify


作者:Tadm
链接:juejin.cn/post/701885…

基本知识

以下信息来自 MDN,相对全面易懂

定义

JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。

语法

JSON.stringify(value[, replacer [, space]])
复制代码
复制代码

参数

value

将要序列化成 一个 JSON 字符串的值。

replacer 可选

如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;

如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;

如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。

space 可选

指定缩进用的空白字符串,用于美化输出(pretty-print);

如果参数是个数字,它代表有多少的空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;

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

一般用的较多的是 value, replacer可根据需要进行个性化配置。

返回值

一个表示给定值的JSON字符串

异常

  • 当在循环引用时会抛出异常TypeError ("cyclic object value")(循环对象值)
  • 当尝试去转换 BigInt 类型的值会抛出TypeError ("BigInt value can't be serialized in JSON")(BigInt值不能JSON序列化)

特性

特性一

1.undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)

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

const obj = {
    a: 1,
    b: undefined,   // ignore
    c: '11',
    d() {
        console.log('this is func');
    },   // ignore
    e: Symbol('tadm')   // ignore
}

console.log(JSON.stringify(obj));
// {"a":1,"c":"11"}

console.log(JSON.stringify([1, '11', undefined, () => 1 + 1, Symbol('tadm')]));
// [1,"11",null,null,null]
复制代码
复制代码

3.函数、undefined 被单独转换时,会返回 undefined

console.log(JSON.stringify(undefined));
// undefined
console.log(JSON.stringify(() => 1 + 1));
// undefined
复制代码
复制代码

4.NaN 和 Infinity 格式的数值及 null 都会被当做 null

console.log(JSON.stringify(NaN));
// null
console.log(JSON.stringify(Infinity));
// null
console.log(JSON.stringify(null));
// null
复制代码
复制代码

特性二

转换值如果有 toJSON() 方法,该方法定义什么值将被序列化,并会忽略其他属性值

console.log(JSON.stringify({
    name: 'toJSON',
    toJSON: () => console.log('this is toJSON')
}));
// this is toJSON

console.log(JSON.stringify({
    name: 'toJSON',
    toJSON: () => console.log('this is toJSON'),
    toJSON: () => 1 + 1,
}));
// 2
复制代码
复制代码

特性三

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

console.log(JSON.stringify(new Boolean(true)));
// true
console.log(JSON.stringify(new Number(1)));
// 1
console.log(JSON.stringify(new String('tadm')));
// "tadm"
复制代码
复制代码

特性四

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

const obj = {
    a: 1,
}
obj.b = obj;

console.log(JSON.stringify(obj));
// TypeError: Converting circular structure to JSON
//     --> starting at object with constructor 'Object'
//     --- property 'f' closes the circle
复制代码
复制代码

特性五

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

const date = new Date();
console.log(JSON.stringify(date), date.toISOString());
// "2021-10-14T07:34:10.112Z" 2021-10-14T07:34:10.112Z
复制代码
复制代码

特性六

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

复制代码
复制代码

特性七

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

const a = new Map();
a.set('x', 1);
console.log(a, '------', JSON.stringify(a));
// Map(1) { 'x' => 1 } ------ {}
// 其他效果一样,不一一列举

const obj = Object.create(null, {
    x: { value: 1 },
    y: { value: 2, enumerable: true }
});
console.log(Object.getOwnPropertyDescriptors(obj));
/**
 * {
  x: { value: 1, writable: false, enumerable: false, configurable: false },
  y: { value: 2, writable: false, enumerable: true, configurable: false }
}
**/
console.log(JSON.stringify(obj));
// {"y":2}
复制代码
复制代码

特性八

转换 BigInt 时也会抛出错误

JSON.stringify(BigInt('666'))
// Uncaught TypeError: Do not know how to serialize a BigInt
复制代码
复制代码

进阶使用

replacer

函数

const foo = {
    foundation: "Mozilla",
    model: "box",
    week: 45,
    transport: "car",
    month: 7
};

const replacer = (key, value) => {
    if (typeof value === "string") {
        return undefined;
    }
    return value;
}

console.log(JSON.stringify(foo, replacer));
// {"week":45,"month":7}
复制代码
复制代码

数组

console.log(JSON.stringify(foo, ['foundation', 'transport']));
// {"foundation":"Mozilla","transport":"car"}
复制代码
复制代码

space

console.log(JSON.stringify(foo, null, 2));  // 显示两个空格
/**
 * 
{
  "foundation": "Mozilla",
  "model": "box",
  "week": 45,
  "transport": "car",
  "month": 7
}
**/
复制代码
复制代码

源码探析

大概看了一下,原理其实就是根据上述几大特性进行特殊处理,否则直接调用本身的 toString() 方法

实现

const stringify = (data) => {
    const isCyclic = (obj) => {
        let set = new Set();
        let flag = false;

        const deep = (obj) => {
            if (obj && typeof obj != 'object') {
                return;
            }
            if (set.has(obj)) {
                return flag = true;
            }
            set.add(obj);

            // fix Object.create(null) 的情况
            if(!obj?.hasOwnProperty) {
                obj.hasOwnProperty = new Object().hasOwnProperty;
            }

            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    deep(obj[key]);
                }
            }
            // 判断完移除掉
            set.delete(obj);
        }

        deep(obj);

        return flag;
    }

    if (isCyclic(data)) {
        throw new TypeError('Converting circular structure to JSON');
    }

    if (typeof data === 'bigint') {
        throw new TypeError('Do not know how to serialize a BigInt');
    }

    const type = typeof data;
    const commonKeys1 = ['undefined', 'function', 'symbol'];
    const commonKeys2 = [NaN, Infinity, null];
    const getType = (s) => {
        return Object.prototype.toString.call(s).replace(/[object (.*?)]/, '$1').toLowerCase()
    };

    if (type !== 'object' || data === null) {
        let result = data;
        if (commonKeys2.includes(data)) {
            result = null;
        } else if (commonKeys1.includes(type)) {
            return undefined;
        } else if (type === 'string') {
            result = `"${data}"`;
        }

        return String(result);
    } else if (type === 'object') {
        // 对象本身有 toJSON 方法
        if (typeof data.toJSON === 'function') {
            // 将其结果返回再执行上述流程即可
            return stringify(data.toJSON());
        } else if (Array.isArray(data)) {
            let result = data.map((it) => {
                return commonKeys1.includes(typeof it) ? 'null' : stringify(it);
            })

            return `[${result}]`.replace(/'/g, '"');
        } else {
            // 布尔值、数字、字符串的包装对象
            if (['boolean', 'number'].includes(getType(data))) {
                return String(data);
            } else if (getType(data) === 'string') {
                return `"${data}"`;
            } else {
                let result = [];
                // 遍历所有的 key
                Object.keys(data).forEach((key) => {
                    if (typeof key !== 'symbol') {
                        const value = data[key];

                        if (!commonKeys1.includes(typeof value)) {
                            // 注意:如果 value 还是对象型的需要递归处理
                            result.push(`"${key}":${stringify(value)}`)
                        }
                    }
                })

                return `{${result}}`.replace(/'/, '"');
            }
        }
    }
}
复制代码
复制代码

测试

console.log(stringify({
    toJSON: () => 1 + 1
}))
// 2

const obj = {
    a: 1,
    b: undefined,
    c: '11',
    d() {
        console.log('this is func');
    },
    e: Symbol('tadm'),
    // g: BigInt('666')
}
// obj.f = obj;

const obj = Object.create(null, {
    x: { value: 1 },
    y: { value: 2, enumerable: true }
});

console.log(stringify(obj));
// {"a":1,"c":"11"}
// {"y":2}
复制代码
复制代码

End

其实 JSON.stringify 只是众多常用 API 中的一个,平时会经常使用它操作对象,比如使用 localStorage 的时候,因为它只能存储字符串,我们恰好可以使其将对象转化为字符串;但是也如上面提到的那些特性,我们很容易会忽略掉非正常数据(undefined/null/BigInt等)的转化结果,所以在使用时应当格外注意。

分类:
前端
收藏成功!
已添加到「」, 点击更改