This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

preface

Be in awe of the online environment. Any one of these points can cause an online breakdown and get you out of your annual bonus. For example, we use json.stringify, an extremely familiar but unfamiliar API.

After reading this article, you can learn:

  1. Learn about a sad story that nearly cost me my annual bonus O (╥﹏╥) O
  2. Learn 9 features and conversion rules for Json.stringify (key)
  3. Learn how to determine if an object has a circular reference (important)
  4. Write a json.stringify from scratch (emphasis)
  5. , etc.

Tell a sad story

Recently, a colleague of the team resigned, and I was responsible for the maintenance of a piece of business he was in charge of. As a result, I just took over, and the code was not yet covered, so I almost carried on p0. Let me take a moment to tell you the whole story.

At the beginning of sad

One day bighead was swimming in code when he was suddenly pulled into an online problem solving group, which was not without its buzz.

Product students are complaining: Online users cannot submit forms, which brings a lot of customer complaints. It may be p0 fault, hope to solve it as soon as possible.

Test students in wonder: this scene test and pre-release environment clearly tested, how the line is not good.

The interface is missing the value field, which causes an error.

Just no one says how to solve the problem!!

Just no one says how to solve the problem!!

Just no one says how to solve the problem!!

Do you have deja vu of this scene? O (╥﹏╥) O, no matter how the first priority is to solve the online problem, reduce the continuing impact, quickly turn out the handover code, began the process of investigation.

Question why

You have a dynamic form collection page where the user selects or fills in the information (fields can be submitted if they are not required), and the front end sends the data to the back end.

Direct error cause

If this parameter is not mandatory, the string object after json. stringify in the signInfo field lacks a value key. As a result, the back end parse cannot read the value correctly and reports an exception to the interface system.

// There is no value key in array string
{
  signInfo: '[{"fieldId":539},{"fieldId":540},{"fieldId":546,"value":"10:30"}]'
}

// Enter the parameter data normally
{
  signInfo'[{" fieldId: "539," value ":" silver "}, {" fieldId: "540," value ":" 2021-03-01 "}, {" fieldId: "546," value ":" at 10:30}]'
}

Copy the code

How is abnormal data generated

// By default the data looks like this
let signInfo = [
  {
    fieldId: 539.value: undefined
  },
  {
    fieldId: 540.value: undefined
  },
  {
    fieldId: 546.value: undefined},]// After json.stringify, the value key is missing, so the back end cannot read the value and report an error
// The reason is that 'undefined', 'arbitrary function', and 'symbol' values are ignored in the serialization process when they appear in the attribute values of 'non-array objects'
console.log(JSON.stringify(signInfo))
// '[{"fieldId":539},{"fieldId":540},{"fieldId":546}]'

Copy the code

The solution

The cause of the problem is found, and the solution (here is only the front-end solution, of course, can also be solved by the back-end) is also very simple, the item whose value is undefined is converted to an empty string and then submitted.

Plan 1: open a new object processing

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":""}]'

Copy the code

Option 2: Use the second parameter json. stringify, directly processing

The flaw of scheme 1 is that it requires a new object to operate, which is not elegant enough

let signInfo = [
  {
    fieldId: 539.value: undefined
  },
  {
    fieldId: 540.value: undefined
  },
  {
    fieldId: 546.value: undefined},]// If value is undefined, return an empty string
JSON.stringify(signInfo, (key, value) = > typeof value === 'undefined' ? ' ' : value)
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'

Copy the code

The story of subsequent

This was a page that had been up for a while, so why did this suddenly happen and not before? After careful inquiry, it turned out that the product classmate proposed a small optimization point in the middle of the process, and the resigned friend felt that the point was too small and directly changed the code to go online, but did not think there was an online problem.

Behind this matter from the product to the test, to the back end, to the front end alone to do a complete review, the details will not expand to say.

Because from the discovery of problems to solve the problem faster, the impact of fewer users, has not reached the degree of accountability, my year-end bonus can be saved O (╥﹏╥) O.

Relearn JSON. Stringify

After this incident, I felt compelled to take a fresh look at the json.stringify method, thoroughly understand the conversion rules, and try to implement a json.stringify by hand

If you have encountered the same problem as me, welcome to study again, there will be a different harvest oh!

Learn through JSON. Stringify

The json.stringify () method converts a JavaScript object or value to a JSON string, optionally replacing the value if a replacer function is specified, or optionally containing only the properties specified by the array if the specified replacer is an array.

The following information is from MDN

grammar

JSON.stringify(value[, replacer [, space]])
Copy the code

parameter

  • 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 is converted and processed by the function during serialization;
    2. If the parameter is an array, only the property names contained in the array will be serialized into the final JSON string;
    3. If this parameter is null or not provided, all attributes of the object are serialized.
  • Space optional

    1. Specifies a blank string for indentation, which is used to beautify the output (pretty-print).
    2. If the argument is a number, it represents how many Spaces there are; The upper limit is 10.
    3. If the value is less than 1, there are no Spaces.
    4. If the parameter is a string (the first 10 characters are taken when the string is longer than 10 characters), the string will be used as a space.
    5. If this parameter is not provided (or null), there will be no Spaces.

The return value

A JSON string representing the given value.Copy the code

abnormal

  • An exception TypeError (” Cyclic Object Value “) is thrown when a cyclic reference is made
  • When attempting to convert a value of type BigInt, TypeError is raised (“BigInt value can’t be serialized in JSON”).

The basic use

Pay attention to

  1. Json.stringify can convert objects or values (more commonly used to convert objects)
  2. You can specifyreplacerSelectively substituting for functions
  3. You can also specifyreplacerFor an array, you can convert the specified properties

Json.stringify: NDN: json.stringify: NDN: json.stringify

// 1. Convert objects
console.log(JSON.stringify({ name: 'Front End Bighead'.sex: 'boy' })) // '{"name":" head ","sex":"boy"}'

// 2. Convert normal values
console.log(JSON.stringify('Front End Bighead')) // "front-end bighead fish"
console.log(JSON.stringify(1)) / / "1"
console.log(JSON.stringify(true)) // "true"
console.log(JSON.stringify(null)) // "null"

// 3. Specify the replacer function
console.log(JSON.stringify({ name: 'Front End Bighead'.sex: 'boy'.age: 100 }, (key, value) = > {
  return typeof value === 'number' ? undefined : value
}))
// '{"name":" head ","sex":"boy"}'

// 4. Specify arrays
console.log(JSON.stringify({ name: 'Front End Bighead'.sex: 'boy'.age: 100},'name' ]))
// '{"name":" head "}'

// 5. Specify space(beautify output)
console.log(JSON.stringify({ name: 'Front End Bighead'.sex: 'boy'.age: 100 }))
// '{"name":" head ","sex":"boy","age":100}'
console.log(JSON.stringify({ name: 'Front End Bighead'.sex: 'boy'.age: 100 }, null , 2))
/ * {" name ":" front-end bighead ", "sex" : "boy", "age" : 100} * /
Copy the code

9 features to keep in mind

I only used this method before, but I didn’t know his conversion rules in detail. There were as many as 9 of them.

Features a

  1. undefined,Arbitrary functionAs well asSymbol value, appeared in theNon-array objectIs ignored during serialization
  2. undefined,Arbitrary functionAs well asSymbol valueAppear in theAn array ofWhen will be converted tonull.
  3. undefined,Arbitrary functionAs well asSymbol valuebeSingle conversion, undefined is returned
// 1. The presence of these three values in an object is ignored
console.log(JSON.stringify({
  name: 'Front End Bighead'.sex: 'boy'.// The function is ignored
  showName () {
    console.log('Front End Bighead')},// undefined is ignored
  age: undefined.// Symbol is ignored
  symbolName: Symbol('Front End Bighead')}))// '{"name":" head ","sex":"boy"}'

// 2. There are three values in the array that will be converted to NULL
console.log(JSON.stringify([
  'Front End Bighead'.'boy'.// The function is converted to NULL
  function showName () {
    console.log('Front End Bighead')},//undefined is converted to null
  undefined.//Symbol is converted to NULL
  Symbol('Front End Bighead')))// '["前端胖头鱼","boy",null,null,null]'

// 3. A single conversion returns undefined
console.log(JSON.stringify(
  function showName () {
    console.log('Front End Bighead')}))// undefined
console.log(JSON.stringify(undefined)) // undefined
console.log(JSON.stringify(Symbol('Front End Bighead'))) // undefined

Copy the code

Features two

Booleans, numbers, and string wrapper objects are automatically converted to their original values during serialization.

console.log(JSON.stringify([new Number(1), new String("Front end bighead fish"), new Boolean(false)))// '[1," bighead ",false]'
Copy the code

Features three

All properties with symbol as the property key are completely ignored, even if they are mandatory in the replacer parameter.

console.log(JSON.stringify({
  name: Symbol('Front End Bighead'),}))/ / '{}'
console.log(JSON.stringify({
  [ Symbol('Front End Bighead') :'Front End Bighead',},(key, value) = > {
  if (typeof key === 'symbol') {
    return value
  }
}))
// undefined
Copy the code

Features four

Values and nulls in NaN and Infinity formats are treated as null.

console.log(JSON.stringify({
  age: NaN.age2: Infinity.name: null
}))
// '{"age":null,"age2":null,"name":null}'

Copy the code

Features five

Convert values if there is a toJSON() method, which defines what values will be serialized.

const toJSONObj = {
  name: 'Front End Bighead',
  toJSON () {
    return 'JSON.stringify'}}console.log(JSON.stringify(toJSONObj))
// "JSON.stringify"
Copy the code

Features 6

Date the Date is converted to a string by calling toJSON() (same as date.toisostring ()), so it is treated as a string.

const d = new Date(a)console.log(d.toJSON()) T14: / / 2021-10-05 01:23. 932 z
console.log(JSON.stringify(d)) / / "the 2021-10-05 T14:01:23. 932 z"

Copy the code

Features seven

Executing this method on objects that contain circular references (objects that refer to each other in an infinite loop) throws an error.

let cyclicObj = {
  name: 'Front End Bighead',
}

cyclicObj.obj = cyclicObj

console.log(JSON.stringify(cyclicObj))
// Converting circular structure to JSON
Copy the code

Features eight

Other types of objects, including Map/Set/WeakMap/WeakSet, serialize only enumerable properties

let enumerableObj = {}

Object.defineProperties(enumerableObj, {
  name: {
    value: 'Front End Bighead'.enumerable: true
  },
  sex: {
    value: 'boy'.enumerable: false}})console.log(JSON.stringify(enumerableObj))
// '{"name":" head "}'
Copy the code

Features nine

Error thrown when attempting to convert a value of type BigInt

const alsoHuge = BigInt(9007199254740991)

console.log(JSON.stringify(alsoHuge))
// TypeError: Do not know how to serialize a BigInt
Copy the code

Write a json.stringify by hand

Finally relearning the many features of json.stringify! Let’s write a simple version based on these features (no replacer function and space).

The source code to achieve

const jsonstringify = (data) = > {
  // Check whether an object has a circular reference
  const isCyclic = (obj) = > {
  // Use the Set data type to store detected objects
  let stackSet = new Set(a)let detected = false

  const detect = (obj) = > {
    // If it is not an object type, you can skip it
    if (obj && typeofobj ! ='object') {
      return
    }
    // A circular reference exists when the object to be checked already exists in stackSet
    if (stackSet.has(obj)) {
      return detected = true
    }
    // Save the current obj as stackSet
    stackSet.add(obj)

    for (let key in obj) {
      // Check the attributes under obj one by one
      if (obj.hasOwnProperty(key)) {
        detect(obj[key])
      }
    }
    // After horizontal detection is complete, delete the current object to prevent misjudgment
    } let obj4 = {obj1: tempObj, obj2: tempObj} */ let obj4 = {obj1: tempObj, obj2: tempObj
    stackSet.delete(obj)
  }

  detect(obj)

  return detected
}

  //
  Executing this method on objects that contain circular references (objects that refer to each other in an infinite loop) throws an error.
  if (isCyclic(data)) {
    throw new TypeError('Converting circular structure to JSON')}//
  // An error is thrown when attempting to convert 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()
  }

  / / the object
  if(type ! = ='object' || data === null) {
    let result = data
    //
    // NaN and Infinity values and null are treated as null.
    if ([NaN.Infinity.null].includes(data)) {
      result = 'null'
      //
      // undefined, arbitrary functions, and symbol values are converted separately to return undefined
    } else if (commonKeys1.includes(type)) {
      // Get undefined directly instead of a string 'undefined'
      return undefined
    } else if (type === 'string') {
      result = '"' + data + '"'
    }

    return String(result)
  } else if (type === 'object') {
    //
    If there is a toJSON() method, what value does that method define to be serialized
    //
    // Date the Date calls toJSON() to convert it to a string (same as date.toisostring ()), so it is treated as a string.
    if (typeof data.toJSON === 'function') {
      return jsonstringify(data.toJSON())
    } else if (Array.isArray(data)) {
      let result = data.map((it) = > {
        //
        // 'undefined', 'arbitrary functions', and' symbol 'values are converted to' null 'when appearing in' arrays'
        return commonKeys1.includes(typeof it) ? 'null' : jsonstringify(it)
      })

      return ` [${result}] `.replace(/'/g.'"')}else {
      //
      // Booleans, numbers, and strings are automatically converted to their original values during serialization.
      if (['boolean'.'number'].includes(getType(data))) {
        return String(data)
      } else if (getType(data) === 'string') {
        return '"' + data + '"'
      } else {
        let result = []
        / / features eight
        // Other types of objects, including Map/Set/WeakMap/WeakSet, serialize only enumerable properties
        Object.keys(data).forEach((key) = > {
          //
          // All properties with symbol as the property key are completely ignored, even if they are mandatory in the replacer argument.
          if (typeofkey ! = ='symbol') {
            const value = data[key]
            / / feature
            // 'undefined', 'arbitrary function', and 'symbol' values are ignored in the serialization process when they appear in attribute values of 'non-array objects'
            if(! commonKeys1.includes(typeof value)) {
              result.push(`"${key}":${jsonstringify(value)}`)}}})return ` {${result}} `.replace(/ / '.'"')}}}}Copy the code

Test a

// 1. Test the basic output
console.log(jsonstringify(undefined)) // undefined 
console.log(jsonstringify(() = >{}))// undefined
console.log(jsonstringify(Symbol('Front End Bighead'))) // undefined
console.log(jsonstringify((NaN))) // null
console.log(jsonstringify((Infinity))) // null
console.log(jsonstringify((null))) // null
console.log(jsonstringify({
  name: 'Front End Bighead'.toJSON() {
    return {
      name: 'Bighead Head 2'.sex: 'boy'}}}))// {"name":" bighead 2","sex":"boy"}

// 2. Compare with the native json.stringify conversion
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 = "Front end bighead fish";
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(a);console.log(jsonstringify(date) === JSON.stringify(date));
// true
let sym = Symbol('Front End Bighead');
console.log(jsonstringify(sym) === JSON.stringify(sym));
// true
let array = [1.2.3];
console.log(jsonstringify(array) === JSON.stringify(array));
// true
let obj = {
  name: 'Front End Bighead'.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('Front End Bighead'),
    number: new Number(1),}}console.log(jsonstringify(obj) === JSON.stringify(obj)) 
// true
console.log((jsonstringify(obj)))
// {" name ":" front-end variegated carp ", "age" : 18, "attr" : [" coding ", 123], "date" : "the 2021-10-06 T14:59:58. 306 z", "info" : {" age ": 16," type ": {" job" : null}}, "PakingObj" : {" Boolean ": false," string ":" front-end bighead ", "number" : 1}}
console.log(JSON.stringify(obj))
// {" name ":" front-end variegated carp ", "age" : 18, "attr" : [" coding ", 123], "date" : "the 2021-10-06 T14:59:58. 306 z", "info" : {" age ": 16," type ": {" job" : null}}, "PakingObj" : {" Boolean ": false," string ":" front-end bighead ", "number" : 1}}

// 3. Test traversable objects
let enumerableObj = {}

Object.defineProperties(enumerableObj, {
  name: {
    value: 'Front End Bighead'.enumerable: true
  },
  sex: {
    value: 'boy'.enumerable: false}})console.log(jsonstringify(enumerableObj))
// {"name":" bighead "}

// 4. Test loop references and Bigint

let obj1 = { a: 'aa' }
let obj2 = { name: 'Front End Bighead'.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

Copy the code

As you can see from the above tests, jsonStringify and json.stringify perform basically the same.

At the end

Because of a BUG, I relearned json.stringify and realized that it still has so many features that I didn’t notice. The front end entertainment industry is too deep. I hope everyone is treated with gentleness, less bugs and more care. Good night