就因为JSON.stringify,我的年终奖差点打水漂了

2023-11-02 03:20

本文主要是介绍就因为JSON.stringify,我的年终奖差点打水漂了,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大家好,我是 漫步,前面分享过一篇JSON.stringify的文章,今天来看这个,喜欢记得关注我并设为星标。


前言

开发要对线上环境有一颗敬畏之心,任何一个点都有可能导致线上故障,也有可能让你的年终奖泡汤(⊙︿⊙)。比如使用了JSON.stringify,这个无比熟悉但又无比陌生的API。

看完本文您可以收获:

  1. 了解一个差点让我年终奖打水漂的悲伤的故事o(╥﹏╥)o

  2. 学习JSON.stringify的9大特性和转换规则 「(重点)」

  3. 了解如何判断一个对象是否存在循环引用 「(重点)」

  4. 从零开始手写一个JSON.stringify 「(重点)」

  5. 等等

说一个悲伤的故事

最近组内有个小伙伴离职了,他所负责的一块业务由我去维护,结果刚接手,代码还没捂热乎,差点背上p0的锅。请让我花一点时间和你说清楚来龙去脉。

悲伤伊始

这一天「胖头鱼」正徜徉在代码的海洋不可自拔,突然被拉进了一个线上问题排查群,群里不可谓不热闹。ee47742aa363041ae10f565a72243755.png

「产品同学」在诉苦:线上用户不能提交表单了,带来了好多客诉,估计会是p0故障,希望尽快解决。

「测试同学」在纳闷:这个场景测试和预发环境明明验过的,怎么线上就不行了。

「后端同学」在讲原因:接口缺少了value字段,导致出错了。

就是木有人说问题怎么解决!!!

就是木有人说问题怎么解决!!!

就是木有人说问题怎么解决!!!

这样的场景不知道你是不是也似曾相识呢?o(╥﹏╥)o,不管咋说第一要务还是先把线上问题解决掉,减少持续影响,赶紧把交接的代码翻出来,开始了排查过程。

问题原因

如下图:有这样一个动态表单搜集页面,用户选择或者填写了信息之后(各字段非必填情况下也可以直接提交),接着前端把数据发送给后端,结束,看起来没有多复杂的逻辑。

1ce83334bc2f1b9d149f029221a3306c.png
image.png

「直接错误原因」

非必填情况下,signInfo字段中经过JSON.stringify后的字符串对象缺少value key,导致后端parse之后无法正确读取value值,进而报接口系统异常,用户无法进行下一步动作。

// 异常入参数据,数组字符串中没有value key
{signInfo: '[{"fieldId":539},{"fieldId":540},{"fieldId":546,"value":"10:30"}]'
}// 正常入参数据
{signInfo: '[{"fieldId":539,"value":"银卡"},{"fieldId":540,"value":"2021-03-01"},{"fieldId":546,"value":"10:30"}]'
}

「异常数据是如何产生的」

// 默认情况下数据是这样的
let signInfo = [{fieldId: 539,value: undefined},{fieldId: 540,value: undefined},{fieldId: 546,value: undefined},
]
// 经过JSON.stringify之后的数据,少了value key,导致后端无法读取value值进行报错
// 具体原因是`undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略
console.log(JSON.stringify(signInfo))
// '[{"fieldId":539},{"fieldId":540},{"fieldId":546}]'

解决方案

问题的原因找到了,解决方式 「(这里只讲前端的解决方案,当然也可以由后端解决)」 也很简单,将value值为undefined的项转化为空字符串再提交即可。

「方案一:新开一个对象处理」

let signInfo = [{fieldId: 539,value: undefined},{fieldId: 540,value: undefined},{fieldId: 546,value: undefined},
]let newSignInfo = signInfo.map((it) => {const value = typeof it.value === 'undefined' ? '' : it.valuereturn {...it,value}
})console.log(JSON.stringify(newSignInfo))
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'

方案二:利用JSON.stringify第二个参数,直接处理

方案一的缺陷是需要新开一个对象进行一顿操作才能解决,不够优雅

let signInfo = [{fieldId: 539,value: undefined},{fieldId: 540,value: undefined},{fieldId: 546,value: undefined},
]// 判断到value为undefined,返回空字符串即可
JSON.stringify(signInfo, (key, value) => typeof value === 'undefined' ? '' : value)
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'

故事后续

原本这是一个已经上线有一段时间的页面,为何会突然出现这个问题,之前却没有呢?仔细询问下,原来是中途产品同学提了一个小的优化点,离职的小伙伴感觉点比较小直接就改了代码上线了,未曾想出现了线上问题。

后面针对这件事从产品到测试、到后端、到前端单独做了一个完整的复盘,细节就不再展开说了。

因为从发现问题到解决问题速度较快、影响用户数较少,还未达到问责程度,「俺的年终奖可算是保住了o(╥﹏╥)o。」

重学JSON.stringify

经过这件事情,我觉得有必要重新审视一下JSON.stringify这个方法,彻底搞清楚转换规则,并尝试手写实现一个JSON.stringify

如果你曾遇到和我一样的问题,欢迎一起来重新学习一次,一定会有不一样的收获噢!

学透JSON.stringify

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

以下信息来自MDN

语法

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

参数[1]

  • value

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

  • replacer 可选

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

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

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

space 可选

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

  2. 如果参数是个数字,它代表有多少的空格;上限为10。

  3. 该值若小于1,则意味着没有空格;

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

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

「返回值」

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

异常[2]

  • 当在循环引用时会抛出异常TypeError ("cyclic object value")(循环对象值)

  • 当尝试去转换 BigInt 类型的值会抛出TypeError ("BigInt value can't be serialized in JSON")(BigInt值不能JSON序列化).

基本使用

「注意」

  1. JSON.stringify可以转换对象或者值(平常用的更多的是转换对象)

  2. 可以指定replacer为函数选择性的地替换

  3. 也可以指定replacer为数组,可转换指定的属性

这里仅仅是NDN上关于JSON.stringify其中最基础的说明,咱们先打个码试试这几个特性

// 1. 转换对象
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy' })) // '{"name":"前端胖头鱼","sex":"boy"}'// 2. 转换普通值
console.log(JSON.stringify('前端胖头鱼')) // "前端胖头鱼"
console.log(JSON.stringify(1)) // "1"
console.log(JSON.stringify(true)) // "true"
console.log(JSON.stringify(null)) // "null"// 3. 指定replacer函数
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, (key, value) => {return typeof value === 'number' ? undefined : value
}))
// '{"name":"前端胖头鱼","sex":"boy"}'// 4. 指定数组
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, [ 'name' ]))
// '{"name":"前端胖头鱼"}'// 5. 指定space(美化输出)
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }))
// '{"name":"前端胖头鱼","sex":"boy","age":100}'
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, null , 2))
/*
{"name": "前端胖头鱼","sex": "boy","age": 100
}
*/

9大特性要记住

以前仅仅是使用了这个方法,却没有详细了解他的转换规则,居然有9个之多。

特性一
  1. undefined任意的函数以及symbol值,出现在非数组对象的属性值中时在序列化过程中会被忽略

  2. undefined任意的函数以及symbol值出现在数组中时会被转换成 null

  3. undefined任意的函数以及symbol值单独转换时,会返回 undefined

// 1. 对象中存在这三种值会被忽略
console.log(JSON.stringify({name: '前端胖头鱼',sex: 'boy',// 函数会被忽略showName () {console.log('前端胖头鱼')},// undefined会被忽略age: undefined,// Symbol会被忽略symbolName: Symbol('前端胖头鱼')
}))
// '{"name":"前端胖头鱼","sex":"boy"}'// 2. 数组中存在着三种值会被转化为null
console.log(JSON.stringify(['前端胖头鱼','boy',// 函数会被转化为nullfunction showName () {console.log('前端胖头鱼')},//undefined会被转化为nullundefined,//Symbol会被转化为nullSymbol('前端胖头鱼')
]))
// '["前端胖头鱼","boy",null,null,null]'// 3.单独转换会返回undefined
console.log(JSON.stringify(function showName () {console.log('前端胖头鱼')}
)) // undefined
console.log(JSON.stringify(undefined)) // undefined
console.log(JSON.stringify(Symbol('前端胖头鱼'))) // undefined
特性二

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

console.log(JSON.stringify([new Number(1), new String("前端胖头鱼"), new Boolean(false)]))
// '[1,"前端胖头鱼",false]'
特性三

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

console.log(JSON.stringify({name: Symbol('前端胖头鱼'),
}))
// '{}'
console.log(JSON.stringify({[ Symbol('前端胖头鱼') ]: '前端胖头鱼',
}, (key, value) => {if (typeof key === 'symbol') {return value}
}))
// undefined
特性四

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

console.log(JSON.stringify({age: NaN,age2: Infinity,name: null
}))
// '{"age":null,"age2":null,"name":null}'
特性五

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

const toJSONObj = {name: '前端胖头鱼',toJSON () {return 'JSON.stringify'}
}console.log(JSON.stringify(toJSONObj))
// "JSON.stringify"
特性六

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

const d = new Date()console.log(d.toJSON()) // 2021-10-05T14:01:23.932Z
console.log(JSON.stringify(d)) // "2021-10-05T14:01:23.932Z"
特性七

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

let cyclicObj = {name: '前端胖头鱼',
}cyclicObj.obj = cyclicObjconsole.log(JSON.stringify(cyclicObj))
// Converting circular structure to JSON
特性八

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

let enumerableObj = {}Object.defineProperties(enumerableObj, {name: {value: '前端胖头鱼',enumerable: true},sex: {value: 'boy',enumerable: false},
})console.log(JSON.stringify(enumerableObj))
// '{"name":"前端胖头鱼"}'
特性九

当尝试去转换 BigInt 类型的值会抛出错误

const alsoHuge = BigInt(9007199254740991)console.log(JSON.stringify(alsoHuge))
// TypeError: Do not know how to serialize a BigInt

手写一个JSON.stringify

终于重新学完JSON.stringify的众多特性啦!咱们根据这些特性来手写一个简单版本的吧(「无replacer函数和space」

源码实现

const jsonstringify = (data) => {// 确认一个对象是否存在循环引用const isCyclic = (obj) => {// 使用Set数据类型来存储已经检测过的对象let stackSet = new Set()let detected = falseconst detect = (obj) => {// 不是对象类型的话,可以直接跳过if (obj && typeof obj != 'object') {return}// 当要检查的对象已经存在于stackSet中时,表示存在循环引用if (stackSet.has(obj)) {return detected = true}// 将当前obj存如stackSetstackSet.add(obj)for (let key in obj) {// 对obj下的属性进行挨个检测if (obj.hasOwnProperty(key)) {detect(obj[key])}}// 平级检测完成之后,将当前对象删除,防止误判/*例如:对象的属性指向同一引用,如果不删除的话,会被认为是循环引用let tempObj = {name: '前端胖头鱼'}let obj4 = {obj1: tempObj,obj2: tempObj}*/stackSet.delete(obj)}detect(obj)return detected
}// 特性七:// 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。if (isCyclic(data)) {throw new TypeError('Converting circular structure to JSON')}// 特性九:// 当尝试去转换 BigInt 类型的值会抛出错误if (typeof data === 'bigint') {throw new TypeError('Do not know how to serialize a BigInt')}const type = typeof dataconst commonKeys1 = ['undefined', 'function', 'symbol']const getType = (s) => {return Object.prototype.toString.call(s).replace(/\[object (.*?)\]/, '$1').toLowerCase()}// 非对象if (type !== 'object' || data === null) {let result = data// 特性四:// NaN 和 Infinity 格式的数值及 null 都会被当做 null。if ([NaN, Infinity, null].includes(data)) {result = 'null'// 特性一:// `undefined`、`任意的函数`以及`symbol值`被`单独转换`时,会返回 undefined} else if (commonKeys1.includes(type)) {// 直接得到undefined,并不是一个字符串'undefined'return undefined} else if (type === 'string') {result = '"' + data + '"'}return String(result)} else if (type === 'object') {// 特性五:// 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化// 特性六:// Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。if (typeof data.toJSON === 'function') {return jsonstringify(data.toJSON())} else if (Array.isArray(data)) {let result = data.map((it) => {// 特性一:// `undefined`、`任意的函数`以及`symbol值`出现在`数组`中时会被转换成 `null`return commonKeys1.includes(typeof it) ? 'null' : jsonstringify(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 = []// 特性八// 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性Object.keys(data).forEach((key) => {// 特性三:// 所有以symbol为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。if (typeof key !== 'symbol') {const value = data[key]// 特性一// `undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略if (!commonKeys1.includes(typeof value)) {result.push(`"${key}":${jsonstringify(value)}`)}}})return `{${result}}`.replace(/'/, '"')}}}
}

测试一把

// 1. 测试一下基本输出
console.log(jsonstringify(undefined)) // undefined 
console.log(jsonstringify(() => { })) // undefined
console.log(jsonstringify(Symbol('前端胖头鱼'))) // undefined
console.log(jsonstringify((NaN))) // null
console.log(jsonstringify((Infinity))) // null
console.log(jsonstringify((null))) // null
console.log(jsonstringify({name: '前端胖头鱼',toJSON() {return {name: '前端胖头鱼2',sex: 'boy'}}
}))
// {"name":"前端胖头鱼2","sex":"boy"}// 2. 和原生的JSON.stringify转换进行比较
console.log(jsonstringify(null) === JSON.stringify(null));
// true
console.log(jsonstringify(undefined) === JSON.stringify(undefined));
// true
console.log(jsonstringify(false) === JSON.stringify(false));
// true
console.log(jsonstringify(NaN) === JSON.stringify(NaN));
// true
console.log(jsonstringify(Infinity) === JSON.stringify(Infinity));
// true
let str = "前端胖头鱼";
console.log(jsonstringify(str) === JSON.stringify(str));
// true
let reg = new RegExp("\w");
console.log(jsonstringify(reg) === JSON.stringify(reg));
// true
let date = new Date();
console.log(jsonstringify(date) === JSON.stringify(date));
// true
let sym = Symbol('前端胖头鱼');
console.log(jsonstringify(sym) === JSON.stringify(sym));
// true
let array = [1, 2, 3];
console.log(jsonstringify(array) === JSON.stringify(array));
// true
let obj = {name: '前端胖头鱼',age: 18,attr: ['coding', 123],date: new Date(),uni: Symbol(2),sayHi: function () {console.log("hello world")},info: {age: 16,intro: {money: undefined,job: null}},pakingObj: {boolean: new Boolean(false),string: new String('前端胖头鱼'),number: new Number(1),}
}
console.log(jsonstringify(obj) === JSON.stringify(obj)) 
// true
console.log((jsonstringify(obj)))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}
console.log(JSON.stringify(obj))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}// 3. 测试可遍历对象
let enumerableObj = {}Object.defineProperties(enumerableObj, {name: {value: '前端胖头鱼',enumerable: true},sex: {value: 'boy',enumerable: false},
})console.log(jsonstringify(enumerableObj))
// {"name":"前端胖头鱼"}// 4. 测试循环引用和Bigintlet obj1 = { a: 'aa' }
let obj2 = { name: '前端胖头鱼', a: obj1, b: obj1 }
obj2.obj = obj2console.log(jsonstringify(obj2))
// TypeError: Converting circular structure to JSON
console.log(jsonStringify(BigInt(1)))
// TypeError: Do not know how to serialize a BigInt

通过上面测试可以看出,jsonstringify基本和JSON.stringify表现一致,(也有可能测试用例不够全面,欢迎提出一起学习)

结尾

因为一个BUG,重学了JSON.stringify,了解到原来它还有这么多平时没有注意到特性,前端娱乐圈水太深了,愿大家都被温柔以待,少些bug,多些关怀。晚安

Reference

[1]

Permalink to 参数: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#parameters

[2]

Permalink to 异常: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#%E5%BC%82%E5%B8%B8

最后

转发文章并关注公众号:前端开发博客,回复 1024,领取前端进阶资料

  1. 回复「电子书」领取27本精选电子书

  2. 回复「加群」加入前端大神交流群,一起学习进步

  3. 回复「面试」获取 面试 精选文章

f907925e14e1009703a4a7c2684b4cf3.png

分享和在看就是最大的支持❤️

这篇关于就因为JSON.stringify,我的年终奖差点打水漂了的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/327761

相关文章

MySQL 中查询 VARCHAR 类型 JSON 数据的问题记录

《MySQL中查询VARCHAR类型JSON数据的问题记录》在数据库设计中,有时我们会将JSON数据存储在VARCHAR或TEXT类型字段中,本文将详细介绍如何在MySQL中有效查询存储为V... 目录一、问题背景二、mysql jsON 函数2.1 常用 JSON 函数三、查询示例3.1 基本查询3.2

使用Python将JSON,XML和YAML数据写入Excel文件

《使用Python将JSON,XML和YAML数据写入Excel文件》JSON、XML和YAML作为主流结构化数据格式,因其层次化表达能力和跨平台兼容性,已成为系统间数据交换的通用载体,本文将介绍如何... 目录如何使用python写入数据到Excel工作表用Python导入jsON数据到Excel工作表用

Python获取中国节假日数据记录入JSON文件

《Python获取中国节假日数据记录入JSON文件》项目系统内置的日历应用为了提升用户体验,特别设置了在调休日期显示“休”的UI图标功能,那么问题是这些调休数据从哪里来呢?我尝试一种更为智能的方法:P... 目录节假日数据获取存入jsON文件节假日数据读取封装完整代码项目系统内置的日历应用为了提升用户体验,

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

JSON Web Token在登陆中的使用过程

《JSONWebToken在登陆中的使用过程》:本文主要介绍JSONWebToken在登陆中的使用过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录JWT 介绍微服务架构中的 JWT 使用结合微服务网关的 JWT 验证1. 用户登录,生成 JWT2. 自定义过滤

Java利用JSONPath操作JSON数据的技术指南

《Java利用JSONPath操作JSON数据的技术指南》JSONPath是一种强大的工具,用于查询和操作JSON数据,类似于SQL的语法,它为处理复杂的JSON数据结构提供了简单且高效... 目录1、简述2、什么是 jsONPath?3、Java 示例3.1 基本查询3.2 过滤查询3.3 递归搜索3.4

如何自定义Nginx JSON日志格式配置

《如何自定义NginxJSON日志格式配置》Nginx作为最流行的Web服务器之一,其灵活的日志配置能力允许我们根据需求定制日志格式,本文将详细介绍如何配置Nginx以JSON格式记录访问日志,这种... 目录前言为什么选择jsON格式日志?配置步骤详解1. 安装Nginx服务2. 自定义JSON日志格式各

python dict转换成json格式的实现

《pythondict转换成json格式的实现》本文主要介绍了pythondict转换成json格式的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下... 一开始你变成字典格式data = [ { 'a' : 1, 'b' : 2, 'c编程' : 3,

Java实现XML与JSON的互相转换详解

《Java实现XML与JSON的互相转换详解》这篇文章主要为大家详细介绍了如何使用Java实现XML与JSON的互相转换,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. XML转jsON1.1 代码目的1.2 代码实现2. JSON转XML3. JSON转XML并输出成指定的

Jackson库进行JSON 序列化时遇到了无限递归(Infinite Recursion)的问题及解决方案

《Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursion)的问题及解决方案》使用Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursi... 目录解决方案‌1. 使用 @jsonIgnore 忽略一个方向的引用2. 使用 @JsonManagedR