Preface
" public account. Maybe you have never met before, but it is very likely that you are too late to meet.
Development must have a sense of awe for the online environment, any point may cause online failures, and it may also make your year-end awards go to waste (⊙︿⊙). For example, JSON.stringify
, this very familiar but very unfamiliar API.
After reading this article, you can gain:
- Understand a sad story that almost made my year-end prize go by o(╥﹏╥)o
- Learn the 9 major features and conversion rules of (emphasis)
- Learn how to determine whether an object has a circular reference (emphasis)
- Handwriting a JSON from scratch.stringify (emphasis)
- and many more
Tell a sad story
Recently, a small partner in the group left his job. I maintained a piece of the business he was in charge of. As a result, he just took over, and the code has not been warmed up yet, and he almost carried the pot of p0 on his back. Please let me take a moment to explain the ins and outs to you.
The beginning of grief
On this day, was wandering in the ocean of codes and was unable to extricate itself. Suddenly, he was pulled into an online troubleshooting group, and the group was not unbelievably lively.
Product classmate is complaining: online users can't submit the form, which has brought a lot of customer complaints. It is estimated that it will be a p0 fault, and I hope to solve it as soon as possible.
test classmate wondering: This scene test and pre-release environment have been clearly tested, so how can it be online?
Back-end student is talking about the reason: the interface lacks the value field, which leads to an error.
is that some people say how to solve the problem!!!
is that some people say how to solve the problem!!!
is that some people say how to solve the problem!!!
I don’t know if you are familiar with this scene? o(╥﹏╥)o, no matter what the first priority is to solve the online problem first to reduce the continuous impact, quickly turn out the handover code and start the investigation process.
problem causes
As shown in the figure below: there is such a dynamic form collection page, after the user selects or fills in the information ( can also be submitted directly if the fields are not required), then the front end sends the data to the back end, and it ends. It doesn't seem to be too complicated logic.
Direct cause of error
If it is not required, the string objectJSON.stringify
value
key, which causes the backend parse to fail to read the value value correctly, and then reports an interface system exception, and the user cannot proceed with the next action.
// 异常入参数据,数组字符串中没有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"}]'
}
How the abnormal data is generated
// 默认情况下数据是这样的
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}]'
solution
The cause of the problem has been found, and the solution is (here only the front-end solution is discussed, of course, it can also be solved by the back-end) is also very simple, just convert the item with the value of undefined into an empty string and submit it.
Solution 1: Open a new object to process
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":""}]'
Solution 2: Use the JSON.stringify
to process directly
The defect of solution one is that you need to open a new object and perform a meal operation to solve it. not elegant enough
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":""}]'
Follow-up story
Originally, this is a page that has been online for a while. Why did this problem suddenly appear, but it didn't exist before? After careful inquiry, it turned out that the midway product classmates mentioned a small optimization point. The resigned partners felt that the point was relatively small and directly changed the code and went online. There was no online problem.
Later, I will do a complete review of this matter from product to test, to back-end, to front-end. The details will not be discussed again.
Because the speed from problem discovery to problem resolution is faster and the number of users affected is small, and the accountability level has not yet been reached, can be regarded as saved o(╥﹏╥)o.
Relearn JSON.stringify
After this incident, I think it is necessary to re-examineJSON.stringify
, thoroughly figure out the conversion rules, and try to implement aJSON.stringify
If you have encountered the same problem as me, please come and learn again together, you will definitely have a different harvest!
Learn through JSON.stringify
JSON.stringify()
method converts aJavaScript object or
value into a JSON string. If a replacer function is specified, the value can be selectively replaced, or if the specified replacer is an array, it can optionally contain only the attributes specified by the array.
The following information comes from MDN
grammar
JSON.stringify(value[, replacer [, space]])
value
The value to be serialized into a JSON string.
replacer
optional- If the parameter is a function, each attribute of the serialized value will be converted and processed by the function during the serialization process;
- If the parameter is an array, only the attribute names contained in this array will be serialized to the final JSON string;
- If this parameter is null or not provided, all properties of the object will be serialized.
space
optional- Specify the blank string used for indentation to beautify the output (pretty-print);
- If the parameter is a number, it represents how many spaces there are; the upper limit is 10.
- If the value is less than 1, it means that there are no spaces;
- If the parameter is a string (when the string length exceeds 10 letters, take the first 10 letters), the string will be treated as a space;
- If this parameter is not provided (or null), there will be no spaces.
return value
一个表示给定值的JSON字符串。
- An exception will be thrown when cyclic reference
TypeError
("cyclic object value") (cyclic object value) - When trying to convert the value of type
BigInt
TypeError
("BigInt value can't be serialized in JSON") (BigInt value can't be serialized in JSON").
Basic use
Note
- JSON.stringify can convert objects or values (more commonly used conversion objects)
- You can specify
replacer
as the function to selectively replace - You can also specify
replacer
as an array to convert the specified attributes
Here is just the on NDN 1617205972b85b. Let’s try these features by 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 characteristics to remember
I just used this method before, but didn't understand his conversion rules in detail. There were actually 9 of them.
Feature One
undefined
,arbitrary function and
symbol value, when it appears in
non-array object, it will be ignored in the serialization process
- Any function of
undefined
,, and
symbol value will be converted to
null
when they appear in thearray.
undefined
,arbitrary function and
symbol value is
separately by 1617205972b973, it will return 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',
// 函数会被转化为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
Feature Two
Boolean value, number, and string will be automatically converted into the corresponding original value during the serialization process.
console.log(JSON.stringify([new Number(1), new String("前端胖头鱼"), new Boolean(false)]))
// '[1,"前端胖头鱼",false]'
Feature Three
Allsymbol
as the attribute key will be completely ignored, even if thereplacer
parameter is mandatory to include them.
console.log(JSON.stringify({
name: Symbol('前端胖头鱼'),
}))
// '{}'
console.log(JSON.stringify({
[ Symbol('前端胖头鱼') ]: '前端胖头鱼',
}, (key, value) => {
if (typeof key === 'symbol') {
return value
}
}))
// undefined
Feature four
Values and null in NaN and Infinity formats will be treated as null.
console.log(JSON.stringify({
age: NaN,
age2: Infinity,
name: null
}))
// '{"age":null,"age2":null,"name":null}'
Feature Five
If the converted value has a toJSON() method, the method defines what value will be serialized.
const toJSONObj = {
name: '前端胖头鱼',
toJSON () {
return 'JSON.stringify'
}
}
console.log(JSON.stringify(toJSONObj))
// "JSON.stringify"
Feature Six
Date is called toJSON() to convert it to a string (same as Date.toISOString()), so it will be treated as a string.
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"
Feature Seven
Executing this method on objects that contain circular references (objects refer to each other to form an infinite loop) will throw an error.
let cyclicObj = {
name: '前端胖头鱼',
}
cyclicObj.obj = cyclicObj
console.log(JSON.stringify(cyclicObj))
// Converting circular structure to JSON
Feature Eight
Other types of objects, including Map/Set/WeakMap/WeakSet, will only serialize enumerable properties
let enumerableObj = {}
Object.defineProperties(enumerableObj, {
name: {
value: '前端胖头鱼',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})
console.log(JSON.stringify(enumerableObj))
// '{"name":"前端胖头鱼"}'
Feature Nine
When trying to convert BigInt
type value will throw an error
const alsoHuge = BigInt(9007199254740991)
console.log(JSON.stringify(alsoHuge))
// TypeError: Do not know how to serialize a BigInt
Write a JSON.stringify by hand
Finally JSON.stringify
the many features of 0617205972bdbb! Let's write a simple version based on these characteristics ( without replacer function and space )
Source code implementation
const jsonstringify = (data) => {
// 确认一个对象是否存在循环引用
const isCyclic = (obj) => {
// 使用Set数据类型来存储已经检测过的对象
let stackSet = new Set()
let detected = false
const detect = (obj) => {
// 不是对象类型的话,可以直接跳过
if (obj && typeof obj != 'object') {
return
}
// 当要检查的对象已经存在于stackSet中时,表示存在循环引用
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])
}
}
// 平级检测完成之后,将当前对象删除,防止误判
/*
例如:对象的属性指向同一引用,如果不删除的话,会被认为是循环引用
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 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 格式的数值及 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(/'/, '"')
}
}
}
}
Test a handful
// 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. 测试循环引用和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
From the above test, it can be seen that jsonstringify
basically JSON.stringify
(it is also possible that the test cases are not comprehensive enough, welcome to suggest and study together)
end
Because of a bug, I re-learned JSON.stringify
and learned that it still has so many features that I usually didn't notice. The front-end entertainment circle is too deep. I hope everyone will be treated with gentleness, fewer bugs, and more care. Good night
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。