我的朋友因為 JSON.stringify 差點丟了獎金
英文 | https://medium.com/frontend-canteen/my-friend-almost-lost-his-year-end-bonus-because-of-json-stringify-9da86961eb9e
翻譯 | 楊小愛
這是發(fā)生在我朋友身上的真實故事,他的綽號叫胖頭。由于JSON.stringify的錯誤使用,他負(fù)責(zé)的其中一個業(yè)務(wù)模塊上線后出現(xiàn)了bug,導(dǎo)致某個頁面無法使用,進(jìn)而影響用戶體驗,差點讓他失去年終獎。
在這篇文章中,我將分享這個悲傷的故事。然后我們還將討論 JSON.stringify 的各種功能,以幫助您避免將來也犯同樣的錯誤。
我們現(xiàn)在開始
故事是這樣的。
他所在的公司,有一位同事離開了,然后胖頭被要求接受離開同事的工作內(nèi)容。
沒想到,在他接手這部分業(yè)務(wù)后不久,項目中就出現(xiàn)了一個bug。
當(dāng)時,公司的交流群里,很多人都在討論這個問題。
產(chǎn)品經(jīng)理先是抱怨:項目中有一個bug,用戶無法提交表單,客戶抱怨這個。請開發(fā)組盡快修復(fù)。
然后測試工程師說:我之前測試過這個頁面,為什么上線后就不行了?
而后端開發(fā)者說:前端發(fā)送的數(shù)據(jù)缺少value字段,導(dǎo)致服務(wù)端接口出錯。
找到同事抱怨后,問題出在他負(fù)責(zé)的模塊上,我的朋友胖頭真的很頭疼。
經(jīng)過一番檢查,我的朋友終于找到了這個錯誤。
事情就是這樣。
發(fā)現(xiàn)頁面上有一個表單允許用戶提交數(shù)據(jù),然后前端應(yīng)該從表單中解析數(shù)據(jù)并將數(shù)據(jù)發(fā)送到服務(wù)器。
表格是這樣的:(下面是我的模擬)
這些字段是可選的。
通常,數(shù)據(jù)應(yīng)如下所示:
let data = {
signInfo: [
{
"fieldId": 539,
"value": "silver card"
},
{
"fieldId": 540,
"value": "2021-03-01"
},
{
"fieldId": 546,
"value": "10:30"
}
]
}
然后它們應(yīng)該轉(zhuǎn)換為:
但問題是,這些字段是可選的。如果用戶沒有填寫某些字段,那么數(shù)據(jù)會變成這樣:
let data = {
signInfo: [
{
"fieldId": 539,
"value": undefined
},
{
"fieldId": 540,
"value": undefined
},
{
"fieldId": 546,
"value": undefined
}
]
}
他們將變成這樣:
JSON.stringify 在轉(zhuǎn)換過程中忽略其值為undefined的字段。
因此,此類數(shù)據(jù)上傳到服務(wù)器后,服務(wù)器無法解析 value 字段,進(jìn)而導(dǎo)致錯誤。
一旦發(fā)現(xiàn)問題,解決方案就很簡單,為了在數(shù)據(jù)轉(zhuǎn)換為 JSON 字符串后保留 value 字段,我們可以這樣做:
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.value
return {
...it,
value
}
})
console.log(JSON.stringify(newSignInfo))
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'
如果發(fā)現(xiàn)某個字段的值為undefined,我們將該字段的值更改為空字符串。
雖然問題已經(jīng)解決了,但是,我們還需要思考這個問題是怎么產(chǎn)生的。
本來這是一個已經(jīng)上線好幾天的頁面,為什么突然出現(xiàn)這個問題?仔細(xì)排查,原來是產(chǎn)品經(jīng)理之前提出了一個小的優(yōu)化點,然后,胖頭對代碼做了一點改動。但是胖頭對 JSON.stringify 的特性并不熟悉,同時,他認(rèn)為改動比較小,所以沒有進(jìn)行足夠的測試,最終導(dǎo)致項目出現(xiàn) bug。
好在他發(fā)現(xiàn)問題后,很快就解決了問題。這個bug影響的用戶少,所以老板沒有責(zé)怪他,我的朋友獎金沒有丟掉,不然,影響大的話,估計獎金真的就沒有了,甚至還會讓他直接離開。
接著,我們一起來了解一下 JSON.stringify,它為啥那么“厲害”,差點把我朋友的獎金都給弄丟了。
了解一下 JSON.stringify
其實,這個bug主要是因為胖頭對JSON.stringify不熟悉造成的,所以,這里我們就一起來分析一下這個內(nèi)置函數(shù)的一些特點。
基本上,JSON.stringify() 方法將 JavaScript 對象或值轉(zhuǎn)換為 JSON 字符串:
同時,JSON.stringify 有以下規(guī)則。
1、如果目標(biāo)對象有toJSON()方法,它負(fù)責(zé)定義哪些數(shù)據(jù)將被序列化。
2、 Boolean、Number、String 對象在字符串化過程中被轉(zhuǎn)換為對應(yīng)的原始值,符合傳統(tǒng)的轉(zhuǎn)換語義。
3、 undefined、Functions 和 Symbols 不是有效的 JSON 值。如果在轉(zhuǎn)換過程中遇到任何此類值,則它們要么被忽略(在對象中找到),要么被更改為 null(當(dāng)在數(shù)組中找到時)。
4、 所有 Symbol-keyed 屬性將被完全忽略
5、 Date的實例通過返回一個字符串來實現(xiàn)toJSON()函數(shù)(與date.toISOString()相同)。因此,它們被視為字符串。
6、 數(shù)字 Infinity 和 NaN 以及 null 值都被認(rèn)為是 null。
7、 所有其他 Object 實例(包括 Map、Set、WeakMap 和 WeakSet)將僅序列化其可枚舉的屬性。
8、找到循環(huán)引用時拋出TypeError(“循環(huán)對象值”)異常。
9、 嘗試對 BigInt 值進(jìn)行字符串化時拋出 TypeError(“BigInt 值無法在 JSON 中序列化”)。
自己實現(xiàn) JSON.stringify
理解一個函數(shù)的最好方法是自己實現(xiàn)它。下面我寫了一個模擬 JSON.stringify 的簡單函數(shù)。
const jsonstringify = (data) => {
// Check if an object has a circular reference
const isCyclic = (obj) => {
// Use a Set to store the detected objects
let stackSet = new Set()
let detected = false
const detect = (obj) => {
// If it is not an object, we can skip it directly
if (obj && typeof obj != 'object') {
return
}
// When the object to be checked already exists in the stackSet,
// it means that there is a circular reference
if (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 data
const 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 ignored
if (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 的特性并自己實現(xiàn)了它。
今天我與你分享這個故事,是希望你以后遇到這個問題,知道怎么處理,不要也犯同樣的錯誤。
如果你覺得有用的話,請點贊我,關(guān)注我,最后,感謝你的閱讀,編程愉快!
作者:前端Q
歡迎關(guān)注微信公眾號 :前端Q