差点因为 JSON.stringify 丢了奖金...

2022-06-27 14:19:18 浏览数 (1)

翻译 | 杨小爱

英文 | https://medium.com/frontend-canteen/my-friend-almost-lost-his-year-end-bonus-because-of-json-stringify-9da86961eb9e

这是发生在我朋友身上的真实故事,他的绰号叫胖头。由于JSON.stringify的错误使用,他负责的其中一个业务模块上线后出现了bug,导致某个页面无法使用,进而影响用户体验,差点让他失去年终奖。

在这篇文章中,我将分享这个悲伤的故事。然后我们还将讨论 JSON.stringify 的各种功能,以帮助您避免将来也犯同样的错误。

我们现在开始

故事是这样的。

他所在的公司,有一位同事离开了,然后胖头被要求接受离开同事的工作内容。

没想到,在他接手这部分业务后不久,项目中就出现了一个bug。

当时,公司的交流群里,很多人都在讨论这个问题。

产品经理先是抱怨:项目中有一个bug,用户无法提交表单,客户抱怨这个。请开发组尽快修复。

然后测试工程师说:我之前测试过这个页面,为什么上线后就不行了?

而后端开发者说:前端发送的数据缺少value字段,导致服务端接口出错。

找到同事抱怨后,问题出在他负责的模块上,我的朋友胖头真的很头疼。

经过一番检查,我的朋友终于找到了这个错误。

事情就是这样。

发现页面上有一个表单允许用户提交数据,然后前端应该从表单中解析数据并将数据发送到服务器。

表格是这样的:(下面是我的模拟)

这些字段是可选的。

通常,数据应如下所示:

代码语言:javascript复制
let data = {signInfo: [    {"fieldId": 539,"value": "silver card"    },    {"fieldId": 540,"value": "2021-03-01"    },    {"fieldId": 546,"value": "10:30"    }  ]}

然后它们应该转换为:

但问题是,这些字段是可选的。如果用户没有填写某些字段,那么数据会变成这样:

代码语言:javascript复制
let data = {signInfo: [    {"fieldId": 539,"value": undefined    },    {"fieldId": 540,"value": undefined    },    {"fieldId": 546,"value": undefined    }  ]}

他们将变成这样:

JSON.stringify 在转换过程中忽略其值为undefined的字段。

因此,此类数据上传到服务器后,服务器无法解析 value 字段,进而导致错误。

一旦发现问题,解决方案就很简单,为了在数据转换为 JSON 字符串后保留 value 字段,我们可以这样做:

代码语言:javascript复制
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":""}]'

如果发现某个字段的值为undefined,我们将该字段的值更改为空字符串。

虽然问题已经解决了,但是,我们还需要思考这个问题是怎么产生的。

本来这是一个已经上线好几天的页面,为什么突然出现这个问题?仔细排查,原来是产品经理之前提出了一个小的优化点,然后,胖头对代码做了一点改动。但是胖头对 JSON.stringify 的特性并不熟悉,同时,他认为改动比较小,所以没有进行足够的测试,最终导致项目出现 bug。

好在他发现问题后,很快就解决了问题。这个bug影响的用户少,所以老板没有责怪他,我的朋友奖金没有丢掉,不然,影响大的话,估计奖金真的就没有了,甚至还会让他直接离开。

接着,我们一起来了解一下 JSON.stringify,它为啥那么“厉害”,差点把我朋友的奖金都给弄丢了。

了解一下 JSON.stringify

其实,这个bug主要是因为胖头对JSON.stringify不熟悉造成的,所以,这里我们就一起来分析一下这个内置函数的一些特点。

基本上,JSON.stringify() 方法将 JavaScript 对象或值转换为 JSON 字符串:

同时,JSON.stringify 有以下规则。

1、如果目标对象有toJSON()方法,它负责定义哪些数据将被序列化。

2、 Boolean、Number、String 对象在字符串化过程中被转换为对应的原始值,符合传统的转换语义。

3、 undefined、Functions 和 Symbols 不是有效的 JSON 值。如果在转换过程中遇到任何此类值,则它们要么被忽略(在对象中找到),要么被更改为 null(当在数组中找到时)。

4、 所有 Symbol-keyed 属性将被完全忽略

5、 Date的实例通过返回一个字符串来实现toJSON()函数(与date.toISOString()相同)。因此,它们被视为字符串。

6、 数字 Infinity 和 NaN 以及 null 值都被认为是 null。

7、 所有其他 Object 实例(包括 Map、Set、WeakMap 和 WeakSet)将仅序列化其可枚举的属性。

8、找到循环引用时抛出TypeError(“循环对象值”)异常。

9、 尝试对 BigInt 值进行字符串化时抛出 TypeError(“BigInt 值无法在 JSON 中序列化”)。

自己实现 JSON.stringify

理解一个函数的最好方法是自己实现它。下面我写了一个模拟 JSON.stringify 的简单函数。

代码语言:javascript复制
const jsonstringify = (data) => {// Check if an object has a circular referenceconst isCyclic = (obj) => {// Use a Set to store the detected objectslet stackSet = new Set()let detected = false
const detect = (obj) => {// If it is not an object, we can skip it directlyif (obj && typeof obj != 'object') {return      }// When the object to be checked already exists in the stackSet, // it means that there is a circular referenceif (stackSet.has(obj)) {return detected = true      }// save current obj to stackSet      stackSet.add(obj)
for (let key in obj) {// check all property of `obj`if (obj.hasOwnProperty(key)) {          detect(obj[key])        }      }// After the detection of the same level is completed, // the current object should be deleted to prevent misjudgment/*        For example: different properties of an object may point to the same reference,        which will be considered a circular reference if not deleted
        let tempObj = {          name: 'bytefish'        }        let obj4 = {          obj1: tempObj,          obj2: tempObj        }      */      stackSet.delete(obj)    }
    detect(obj)
return detected  }
// Throws a TypeError ("cyclic object value") exception when a circular reference is found.if (isCyclic(data)) {throw new TypeError('Converting circular structure to JSON')  }
// Throws a TypeError  when trying to stringify a BigInt value.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// The numbers Infinity and NaN, as well as the value null, are all considered null.if ([NaN, Infinity, null].includes(data)) {      result = 'null'
// undefined, arbitrary functions, and symbol values are converted individually and return undefined    } else if (commonKeys1.includes(type)) {
return undefined    } else if (type === 'string') {      result = '"'   data   '"'    }
return String(result)  } else if (type === 'object') {// If the target object has a toJSON() method, it's responsible to define what data will be serialized.
// The instances of Date implement the toJSON() function by returning a string (the same as date.toISOString()). Thus, they are treated as strings.if (typeof data.toJSON === 'function') {return jsonstringify(data.toJSON())    } else if (Array.isArray(data)) {let result = data.map((it) => {// 3# undefined, Functions, and Symbols are not valid JSON values. If any such values are encountered during conversion they are either omitted (when found in an object) or changed to null (when found in an array).return commonKeys1.includes(typeof it) ? 'null' : jsonstringify(it)      })
return `[${result}]`.replace(/'/g, '"')    } else {// 2# Boolean, Number, and String objects are converted to the corresponding primitive values during stringification, in accord with the traditional conversion semantics.if (['boolean', 'number'].includes(getType(data))) {return String(data)      } else if (getType(data) === 'string') {return '"'   data   '"'      } else {let result = []// 7# All the other Object instances (including Map, Set, WeakMap, and WeakSet) will have only their enumerable properties serialized.Object.keys(data).forEach((key) => {// 4# All Symbol-keyed properties will be completely ignoredif (typeof key !== 'symbol') {const value = data[key]// 3# undefined, Functions, and Symbols are not valid JSON values. If any such values are encountered during conversion they are either omitted (when found in an object) or changed to null (when found in an array).if (!commonKeys1.includes(typeof value)) {              result.push(`"${key}":${jsonstringify(value)}`)            }          }        })
return `{${result}}`.replace(/'/, '"')      }    }  }}

写在最后

从一个 bug 开始,我们讨论了 JSON.stringify 的特性并自己实现了它。

今天我与你分享这个故事,是希望你以后遇到这个问题,知道怎么处理,不要也犯同样的错误。

如果你觉得有用的话,请点赞我,关注我,最后,感谢你的阅读,编程愉快!

我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取

推荐阅读

  • 重磅!Spring Cloud 生态再添新套件:Spring Cloud Tencent
  • 第一次读 “Clean” 系列,并没有觉得这是一本多好的书
  • 盗取超1亿人数据!前亚马逊员工面临20年监禁

··································

你好,我是程序猿DD,10年开发老司机、阿里云MVP、腾讯云TVP、出过书创过业、国企4年互联网6年。从普通开发到架构师、再到合伙人。一路过来,给我最深的感受就是一定要不断学习并关注前沿。只要你能坚持下来,多思考、少抱怨、勤动手,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你看好一个事情,一定是坚持了才能看到希望,而不是看到希望才去坚持。相信我,只要坚持下来,你一定比现在更好!如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯,帮你积累弯道超车的资本。

点击领取2022最新10000T学习资料

0 人点赞