就因為JSON.stringify,我的年終獎差點打水漂了
前言
?開發(fā)要對線上環(huán)境有一顆敬畏之心,任何一個點都有可能導致線上故障,也有可能讓你的年終獎泡湯(⊙︿⊙)。比如使用了
?JSON.stringify,這個無比熟悉但又無比陌生的API。
看完本文您可以收獲:
了解一個差點讓我年終獎打水漂的悲傷的故事o(╥﹏╥)o 學習JSON.stringify的9大特性和轉換規(guī)則 「(重點)」 了解如何判斷一個對象是否存在循環(huán)引用 「(重點)」 從零開始手寫一個JSON.stringify 「(重點)」 等等
說一個悲傷的故事
?最近組內有個小伙伴離職了,他所負責的一塊業(yè)務由我去維護,結果剛接手,代碼還沒捂熱乎,差點背上p0的鍋。請讓我花一點時間和你說清楚來龍去脈。
?
悲傷伊始
?這一天「胖頭魚」正徜徉在代碼的海洋不可自拔,突然被拉進了一個線上問題排查群,群里不可謂不熱鬧。
?
「產(chǎn)品同學」在訴苦:線上用戶不能提交表單了,帶來了好多客訴,估計會是p0故障,希望盡快解決。
「測試同學」在納悶:這個場景測試和預發(fā)環(huán)境明明驗過的,怎么線上就不行了。
「后端同學」在講原因:接口缺少了value字段,導致出錯了。
就是木有人說問題怎么解決!!!
就是木有人說問題怎么解決!!!
就是木有人說問題怎么解決!!!
這樣的場景不知道你是不是也似曾相識呢?o(╥﹏╥)o,不管咋說第一要務還是先把線上問題解決掉,減少持續(xù)影響,趕緊把交接的代碼翻出來,開始了排查過程。
問題原因
?如下圖:有這樣一個動態(tài)表單搜集頁面,用戶選擇或者填寫了信息之后(
?各字段非必填情況下也可以直接提交),接著前端把數(shù)據(jù)發(fā)送給后端,結束,看起來沒有多復雜的邏輯。

「直接錯誤原因」
?非必填情況下,signInfo字段中經(jīng)過
?JSON.stringify后的字符串對象缺少valuekey,導致后端parse之后無法正確讀取value值,進而報接口系統(tǒng)異常,用戶無法進行下一步動作。
//?異常入?yún)?shù)據(jù),數(shù)組字符串中沒有value?key
{
??signInfo:?'[{"fieldId":539},{"fieldId":540},{"fieldId":546,"value":"10:30"}]'
}
//?正常入?yún)?shù)據(jù)
{
??signInfo:?'[{"fieldId":539,"value":"銀卡"},{"fieldId":540,"value":"2021-03-01"},{"fieldId":546,"value":"10:30"}]'
}
「異常數(shù)據(jù)是如何產(chǎn)生的」
//?默認情況下數(shù)據(jù)是這樣的
let?signInfo?=?[
??{
????fieldId:?539,
????value:?undefined
??},
??{
????fieldId:?540,
????value:?undefined
??},
??{
????fieldId:?546,
????value:?undefined
??},
]
//?經(jīng)過JSON.stringify之后的數(shù)據(jù),少了value?key,導致后端無法讀取value值進行報錯
//?具體原因是`undefined`、`任意的函數(shù)`以及`symbol值`,出現(xiàn)在`非數(shù)組對象`的屬性值中時在序列化過程中會被忽略
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.value
??return?{
????...it,
????value
??}
})
console.log(JSON.stringify(newSignInfo))
//?'[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'
方案二:利用JSON.stringify第二個參數(shù),直接處理
?方案一的缺陷是需要新開一個對象進行一頓操作才能解決,
?不夠優(yōu)雅
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":""}]'
故事后續(xù)
?原本這是一個已經(jīng)上線有一段時間的頁面,為何會突然出現(xiàn)這個問題,之前卻沒有呢?仔細詢問下,原來是中途產(chǎn)品同學提了一個小的優(yōu)化點,離職的小伙伴感覺點比較小直接就改了代碼上線了,未曾想出現(xiàn)了線上問題。
?
后面針對這件事從產(chǎn)品到測試、到后端、到前端單獨做了一個完整的復盤,細節(jié)就不再展開說了。
因為從發(fā)現(xiàn)問題到解決問題速度較快、影響用戶數(shù)較少,還未達到問責程度,「俺的年終獎可算是保住了o(╥﹏╥)o。」
重學JSON.stringify
?經(jīng)過這件事情,我覺得有必要重新審視一下
?JSON.stringify這個方法,徹底搞清楚轉換規(guī)則,并嘗試手寫實現(xiàn)一個JSON.stringify
如果你曾遇到和我一樣的問題,歡迎一起來重新學習一次,一定會有不一樣的收獲噢!
學透JSON.stringify
??
JSON.stringify()?方法將一個JavaScript?對象或值轉換為 JSON 字符串,如果指定了一個 replacer 函數(shù),則可以選擇性地替換值,或者指定的 replacer 是數(shù)組,則可選擇性地僅包含數(shù)組指定的屬性。
以下信息來自MDN
語法
JSON.stringify(value[,?replacer?[,?space]])
參數(shù)[1]
value將要序列化成 一個 JSON 字符串的值。
replacer?可選如果該參數(shù)是一個函數(shù),則在序列化過程中,被序列化的值的每個屬性都會經(jīng)過該函數(shù)的轉換和處理; 如果該參數(shù)是一個數(shù)組,則只有包含在這個數(shù)組中的屬性名才會被序列化到最終的 JSON 字符串中; 如果該參數(shù)為 null 或者未提供,則對象所有的屬性都會被序列化。 space?可選指定縮進用的空白字符串,用于美化輸出(pretty-print); 如果參數(shù)是個數(shù)字,它代表有多少的空格;上限為10。 該值若小于1,則意味著沒有空格; 如果該參數(shù)為字符串(當字符串長度超過10個字母,取其前10個字母),該字符串將被作為空格; 如果該參數(shù)沒有提供(或者為 null),將沒有空格。
「返回值」
一個表示給定值的JSON字符串。
異常[2]
當在循環(huán)引用時會拋出異常 TypeError?("cyclic object value")(循環(huán)對象值)當嘗試去轉換? BigInt?類型的值會拋出TypeError?("BigInt value can't be serialized in JSON")(BigInt值不能JSON序列化).
基本使用
「注意」
JSON.stringify可以轉換對象或者值(平常用的更多的是轉換對象) 可以指定 replacer為函數(shù)選擇性的地替換也可以指定 replacer為數(shù)組,可轉換指定的屬性
這里僅僅是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函數(shù)
console.log(JSON.stringify({?name:?'前端胖頭魚',?sex:?'boy',?age:?100?},?(key,?value)?=>?{
??return?typeof?value?===?'number'???undefined?:?value
}))
//?'{"name":"前端胖頭魚","sex":"boy"}'
//?4.?指定數(shù)組
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大特性要記住
?以前僅僅是使用了這個方法,卻沒有詳細了解他的轉換規(guī)則,居然有9個之多。
?
特性一
undefined、任意的函數(shù)以及symbol值,出現(xiàn)在非數(shù)組對象的屬性值中時在序列化過程中會被忽略undefined、任意的函數(shù)以及symbol值出現(xiàn)在數(shù)組中時會被轉換成?null。undefined、任意的函數(shù)以及symbol值被單獨轉換時,會返回 undefined
//?1.?對象中存在這三種值會被忽略
console.log(JSON.stringify({
??name:?'前端胖頭魚',
??sex:?'boy',
??//?函數(shù)會被忽略
??showName?()?{
????console.log('前端胖頭魚')
??},
??//?undefined會被忽略
??age:?undefined,
??//?Symbol會被忽略
??symbolName:?Symbol('前端胖頭魚')
}))
//?'{"name":"前端胖頭魚","sex":"boy"}'
//?2.?數(shù)組中存在著三種值會被轉化為null
console.log(JSON.stringify([
??'前端胖頭魚',
??'boy',
??//?函數(shù)會被轉化為null
??function?showName?()?{
????console.log('前端胖頭魚')
??},
??//undefined會被轉化為null
??undefined,
??//Symbol會被轉化為null
??Symbol('前端胖頭魚')
]))
//?'["前端胖頭魚","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
特性二
??
布爾值、數(shù)字、字符串的包裝對象在序列化過程中會自動轉換成對應的原始值。
console.log(JSON.stringify([new?Number(1),?new?String("前端胖頭魚"),?new?Boolean(false)]))
//?'[1,"前端胖頭魚",false]'
特性三
?所有以
?symbol為屬性鍵的屬性都會被完全忽略掉,即便?replacer?參數(shù)中強制指定包含了它們。
console.log(JSON.stringify({
??name:?Symbol('前端胖頭魚'),
}))
//?'{}'
console.log(JSON.stringify({
??[?Symbol('前端胖頭魚')?]:?'前端胖頭魚',
},?(key,?value)?=>?{
??if?(typeof?key?===?'symbol')?{
????return?value
??}
}))
//?undefined
特性四
?NaN 和 Infinity 格式的數(shù)值及 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"
特性七
?對包含循環(huán)引用的對象(對象之間相互引用,形成無限循環(huán))執(zhí)行此方法,會拋出錯誤。
?
let?cyclicObj?=?{
??name:?'前端胖頭魚',
}
cyclicObj.obj?=?cyclicObj
console.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的眾多特性啦!咱們根據(jù)這些特性來手寫一個簡單版本的吧(「無replacer函數(shù)和space」)
源碼實現(xiàn)
const?jsonstringify?=?(data)?=>?{
??//?確認一個對象是否存在循環(huán)引用
??const?isCyclic?=?(obj)?=>?{
??//?使用Set數(shù)據(jù)類型來存儲已經(jīng)檢測過的對象
??let?stackSet?=?new?Set()
??let?detected?=?false
??const?detect?=?(obj)?=>?{
????//?不是對象類型的話,可以直接跳過
????if?(obj?&&?typeof?obj?!=?'object')?{
??????return
????}
????//?當要檢查的對象已經(jīng)存在于stackSet中時,表示存在循環(huán)引用
????if?(stackSet.has(obj))?{
??????return?detected?=?true
????}
????//?將當前obj存如stackSet
????stackSet.add(obj)
????for?(let?key?in?obj)?{
??????//?對obj下的屬性進行挨個檢測
??????if?(obj.hasOwnProperty(key))?{
????????detect(obj[key])
??????}
????}
????//?平級檢測完成之后,將當前對象刪除,防止誤判
????/*
??????例如:對象的屬性指向同一引用,如果不刪除的話,會被認為是循環(huán)引用
??????let?tempObj?=?{
????????name:?'前端胖頭魚'
??????}
??????let?obj4?=?{
????????obj1:?tempObj,
????????obj2:?tempObj
??????}
????*/
????stackSet.delete(obj)
??}
??detect(obj)
??return?detected
}
??//?特性七:
??//?對包含循環(huán)引用的對象(對象之間相互引用,形成無限循環(huán))執(zhí)行此方法,會拋出錯誤。
??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?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
????//?特性四:
????// NaN 和 Infinity 格式的數(shù)值及 null 都會被當做 null。
????if?([NaN,?Infinity,?null].includes(data))?{
??????result?=?'null'
??????//?特性一:
??????//?`undefined`、`任意的函數(shù)`以及`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`、`任意的函數(shù)`以及`symbol值`出現(xiàn)在`數(shù)組`中時會被轉換成?`null`
????????return?commonKeys1.includes(typeof?it)???'null'?:?jsonstringify(it)
??????})
??????return?`[${result}]`.replace(/'/g,?'"')
????}?else?{
??????//?特性二:
??????//?布爾值、數(shù)字、字符串的包裝對象在序列化過程中會自動轉換成對應的原始值。
??????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 參數(shù)中強制指定包含了它們。
??????????if?(typeof?key?!==?'symbol')?{
????????????const?value?=?data[key]
????????????//?特性一
????????????//?`undefined`、`任意的函數(shù)`以及`symbol值`,出現(xiàn)在`非數(shù)組對象`的屬性值中時在序列化過程中會被忽略
????????????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.?測試循環(huán)引用和Bigint
let?obj1?=?{?a:?'aa'?}
let?obj2?=?{?name:?'前端胖頭魚',?a:?obj1,?b:?obj1?}
obj2.obj?=?obj2
console.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表現(xiàn)一致,(也有可能測試用例不夠全面,歡迎提出一起學習)
結尾
?因為一個BUG,重學了
?JSON.stringify,了解到原來它還有這么多平時沒有注意到特性,前端娛樂圈水太深了,愿大家都被溫柔以待,少些bug,多些關懷。晚安
Reference
Permalink to 參數(shù): 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

