头图

Just because of JSON.stringify, my year-end award was almost lost

前端胖头鱼
中文

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:

  1. Understand a sad story that almost made my year-end prize go by o(╥﹏╥)o
  2. Learn the 9 major features and conversion rules of (emphasis)
  3. Learn how to determine whether an object has a circular reference (emphasis)
  4. Handwriting a JSON from scratch.stringify (emphasis)
  5. 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.
image.png

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.

image.png

Direct cause of error

If it is not required, the string object JSON.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-examine JSON.stringify , thoroughly figure out the conversion rules, and try to implement a JSON.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 a JavaScript 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]])

Parameters

  • value

    The value to be serialized into a JSON string.

  • replacer optional

    1. If the parameter is a function, each attribute of the serialized value will be converted and processed by the function during the serialization process;
    2. If the parameter is an array, only the attribute names contained in this array will be serialized to the final JSON string;
    3. If this parameter is null or not provided, all properties of the object will be serialized.
  • space optional

    1. Specify the blank string used for indentation to beautify the output (pretty-print);
    2. If the parameter is a number, it represents how many spaces there are; the upper limit is 10.
    3. If the value is less than 1, it means that there are no spaces;
    4. 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;
    5. If this parameter is not provided (or null), there will be no spaces.

return value

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

Abnormal

  • 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

  1. JSON.stringify can convert objects or values (more commonly used conversion objects)
  2. You can specify replacer as the function to selectively replace
  3. 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

  1. undefined , arbitrary function and symbol value, when it appears in non-array object, it will be ignored in the serialization process
  2. Any function of undefined , , and symbol value will be converted to null when they appear in the array.
  3. 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

All symbol as the attribute key will be completely ignored, even if the replacer 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
阅读 1.2k

3.1k 声望
4.2k 粉丝
0 条评论
3.1k 声望
4.2k 粉丝
文章目录
宣传栏