前言

开发要对线上环境有一颗敬畏之心,任何一个点都有可能导致线上故障,也有可能让你的年终奖泡汤(⊙︿⊙)。比方应用了JSON.stringify,这个无比相熟但又无比生疏的API。

看完本文您能够播种:

  1. 理解一个差点让我年终奖打水漂的悲伤的故事o(╥﹏╥)o
  2. 学习JSON.stringify的9大个性和转换规则 (重点)
  3. 理解如何判断一个对象是否存在循环援用 (重点)
  4. 从零开始手写一个JSON.stringify (重点)
  5. 等等

说一个悲伤的故事

最近组内有个小伙伴到职了,他所负责的一块业务由我去保护,后果刚接手,代码还没捂热乎,差点背上p0的锅。请让我花一点工夫和你说分明前因后果。

悲伤伊始

这一天胖头鱼正徜徉在代码的陆地不可自拔,忽然被拉进了一个线上问题排查群,群里不堪称不冷落。

产品同学在诉苦:线上用户不能提交表单了,带来了好多客诉,预计会是p0故障,心愿尽快解决。

测试同学在纳闷:这个场景测试和预发环境明明验过的,怎么线上就不行了。

后端同学在讲起因:接口短少了value字段,导致出错了。

就是木有人说问题怎么解决!!!

就是木有人说问题怎么解决!!!

就是木有人说问题怎么解决!!!

这样的场景不晓得你是不是也似曾相识呢?o(╥﹏╥)o,不论咋说第一要务还是先把线上问题解决掉,缩小继续影响,连忙把交接的代码翻出来,开始了排查过程。

问题起因

如下图:有这样一个动静表单收集页面,用户抉择或者填写了信息之后(各字段非必填状况下也能够间接提交),接着前端把数据发送给后端,完结,看起来没有多简单的逻辑。

间接谬误起因

非必填状况下,signInfo字段中通过JSON.stringify后的字符串对象短少value key,导致后端parse之后无奈正确读取value值,进而报接口零碎异样,用户无奈进行下一步动作。
// 异样入参数据,数组字符串中没有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"}]'}

异样数据是如何产生的

// 默认状况下数据是这样的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}]'

解决方案

问题的起因找到了,解决形式 (这里只讲前端的解决方案,当然也能够由后端解决) 也很简略,将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第二个参数,间接解决

计划一的缺点是须要新开一个对象进行一顿操作能力解决,不够优雅
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":""}]'

故事后续

本来这是一个曾经上线有一段时间的页面,为何会忽然呈现这个问题,之前却没有呢?认真询问下,原来是中途产品同学提了一个小的优化点,到职的小伙伴感觉点比拟小间接就改了代码上线了,未曾想呈现了线上问题。

前面针对这件事从产品到测试、到后端、到前端独自做了一个残缺的复盘,细节就不再开展说了。

因为从发现问题到解决问题速度较快、影响用户数较少,还未达到问责水平,俺的年终奖可算是保住了o(╥﹏╥)o。

重学JSON.stringify

通过这件事件,我感觉有必要从新扫视一下JSON.stringify这个办法,彻底搞清楚转换规则,并尝试手写实现一个JSON.stringify

如果你曾遇到和我一样的问题,欢送一起来重新学习一次,肯定会有不一样的播种噢!

学透JSON.stringify

JSON.stringify()  办法将一个 JavaScript 对象转换为 JSON 字符串,如果指定了一个 replacer 函数,则能够选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅蕴含数组指定的属性。

以下信息来自MDN

语法

JSON.stringify(value[, replacer [, space]])

参数

  • value

    将要序列化成 一个 JSON 字符串的值。

  • replacer 可选

    1. 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会通过该函数的转换和解决;
    2. 如果该参数是一个数组,则只有蕴含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;
    3. 如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
  • space 可选

    1. 指定缩进用的空白字符串,用于丑化输入(pretty-print);
    2. 如果参数是个数字,它代表有多少的空格;下限为10。
    3. 该值若小于1,则意味着没有空格;
    4. 如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;
    5. 如果该参数没有提供(或者为 null),将没有空格。

返回值

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

异样

  • 当在循环援用时会抛出异样TypeError ("cyclic object value")(循环对象值)
  • 当尝试去转换 BigInt 类型的值会抛出TypeError ("BigInt value can't be serialized in JSON")(BigInt值不能JSON序列化).

根本应用

留神

  1. JSON.stringify能够转换对象或者值(平时用的更多的是转换对象)
  2. 能够指定replacer为函数选择性的地替换
  3. 也能够指定replacer为数组,可转换指定的属性

这里仅仅是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函数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大个性要记住

以前仅仅是应用了这个办法,却没有具体理解他的转换规则,竟然有9个之多。

个性一

  1. undefined任意的函数以及symbol值,呈现在非数组对象的属性值中时在序列化过程中会被疏忽
  2. undefined任意的函数以及symbol值呈现在数组中时会被转换成 null
  3. undefined任意的函数以及symbol值独自转换时,会返回 undefined
// 1. 对象中存在这三种值会被疏忽console.log(JSON.stringify({  name: '前端胖头鱼',  sex: 'boy',  // 函数会被疏忽  showName () {    console.log('前端胖头鱼')  },  // undefined会被疏忽  age: undefined,  // Symbol会被疏忽  symbolName: Symbol('前端胖头鱼')}))// '{"name":"前端胖头鱼","sex":"boy"}'// 2. 数组中存在着三种值会被转化为nullconsole.log(JSON.stringify([  '前端胖头鱼',  'boy',  // 函数会被转化为null  function showName () {    console.log('前端胖头鱼')  },  //undefined会被转化为null  undefined,  //Symbol会被转化为null  Symbol('前端胖头鱼')]))// '["前端胖头鱼","boy",null,null,null]'// 3.独自转换会返回undefinedconsole.log(JSON.stringify(  function showName () {    console.log('前端胖头鱼')  })) // undefinedconsole.log(JSON.stringify(undefined)) // undefinedconsole.log(JSON.stringify(Symbol('前端胖头鱼'))) // undefined

个性二

布尔值数字字符串的包装对象在序列化过程中会主动转换成对应的原始值。
console.log(JSON.stringify([new Number(1), new String("前端胖头鱼"), new Boolean(false)]))// '[1,"前端胖头鱼",false]'

个性三

所有以symbol为属性键的属性都会被齐全疏忽掉,即使 replacer 参数中强制指定蕴含了它们。
console.log(JSON.stringify({  name: Symbol('前端胖头鱼'),}))// '{}'console.log(JSON.stringify({  [ Symbol('前端胖头鱼') ]: '前端胖头鱼',}, (key, value) => {  if (typeof key === 'symbol') {    return value  }}))// undefined

个性四

NaN 和 Infinity 格局的数值及 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.932Zconsole.log(JSON.stringify(d)) // "2021-10-05T14:01:23.932Z"

个性七

对蕴含循环援用的对象(对象之间互相援用,造成有限循环)执行此办法,会抛出谬误。
let cyclicObj = {  name: '前端胖头鱼',}cyclicObj.obj = cyclicObjconsole.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的泛滥个性啦!咱们依据这些个性来手写一个简略版本的吧(无replacer函数和space

源码实现

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(/'/, '"')      }    }  }}

测试一把

// 1. 测试一下根本输入console.log(jsonstringify(undefined)) // undefined console.log(jsonstringify(() => { })) // undefinedconsole.log(jsonstringify(Symbol('前端胖头鱼'))) // undefinedconsole.log(jsonstringify((NaN))) // nullconsole.log(jsonstringify((Infinity))) // nullconsole.log(jsonstringify((null))) // nullconsole.log(jsonstringify({  name: '前端胖头鱼',  toJSON() {    return {      name: '前端胖头鱼2',      sex: 'boy'    }  }}))// {"name":"前端胖头鱼2","sex":"boy"}// 2. 和原生的JSON.stringify转换进行比拟console.log(jsonstringify(null) === JSON.stringify(null));// trueconsole.log(jsonstringify(undefined) === JSON.stringify(undefined));// trueconsole.log(jsonstringify(false) === JSON.stringify(false));// trueconsole.log(jsonstringify(NaN) === JSON.stringify(NaN));// trueconsole.log(jsonstringify(Infinity) === JSON.stringify(Infinity));// truelet str = "前端胖头鱼";console.log(jsonstringify(str) === JSON.stringify(str));// truelet reg = new RegExp("\w");console.log(jsonstringify(reg) === JSON.stringify(reg));// truelet date = new Date();console.log(jsonstringify(date) === JSON.stringify(date));// truelet sym = Symbol('前端胖头鱼');console.log(jsonstringify(sym) === JSON.stringify(sym));// truelet array = [1, 2, 3];console.log(jsonstringify(array) === JSON.stringify(array));// truelet 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)) // trueconsole.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. 测试循环援用和Bigintlet obj1 = { a: 'aa' }let obj2 = { name: '前端胖头鱼', a: obj1, b: obj1 }obj2.obj = obj2console.log(jsonstringify(obj2))// TypeError: Converting circular structure to JSONconsole.log(jsonStringify(BigInt(1)))// TypeError: Do not know how to serialize a BigInt

通过下面测试能够看出,jsonstringify根本和JSON.stringify体现统一,(也有可能测试用例不够全面,欢送提出一起学习)

结尾

因为一个BUG,重学了JSON.stringify,理解到原来它还有这么多平时没有留神到个性,前端娱乐圈水太深了,愿大家都被温顺以待,少些bug,多些关心。晚安