Только из-за JSON.stringify мой годовой бонус чуть не пропал

внешний интерфейс JavaScript Vue.js
Только из-за JSON.stringify мой годовой бонус чуть не пропал

Эта статья приняла участие"Проект "Звезда раскопок"", чтобы выиграть творческий подарочный пакет и бросить вызов творческим поощрительным деньгам.

предисловие

«Добро пожаловать для обсуждения в области комментариев, официальный представитель NuggetsПроект «Звезда раскопок»После мероприятия в комментариях будет разыграно 100 штук Наггетсов.Подробнее о лотерее читайте в статье о мероприятии».

Разработчики должны благоговейно относиться к онлайн-среде: любая ошибка может привести к онлайн-сбоям, а также лишить вас годового бонуса (⊙︿⊙). такие как использованиеJSON.stringify, это очень знакомый, но очень незнакомый API.

Прочитав эту статью, вы сможете получить:

  1. Узнайте о грустной истории, которая чуть не стоила мне награды в конце года o(╥﹏╥)o
  2. Изучите 9 функций и правила преобразования JSON.stringify(акцент)
  3. Узнайте, как определить, имеет ли объект циклическую ссылку(акцент)
  4. Рукописный ввод JSON.stringify с нуля(акцент)
  5. так далее

рассказать грустную историю

Недавно из группы ушел маленький партнер, а часть бизнеса, за которую он отвечал, вел я, в итоге он просто взял на себя код, а код был еще горячим, и он чуть не попал в котел Р0. Пожалуйста, позвольте мне воспользоваться моментом, чтобы объяснить вам все входы и выходы.

начало печали

этот деньТолстая голова рыбыЯ бродил по морю кода и никак не мог выбраться, и меня внезапно затянуло в онлайн-группу по устранению неполадок, группа была довольно живой.

image.png

одноклассники по продуктуЖалобы: онлайн-пользователи не могут отправить форму, что приносит много жалоб клиентов.Предполагается, что это будет ошибка P0, и я надеюсь решить ее как можно скорее.

тестовые одноклассникиМне вот интересно: этот сценарий был опробован и предпроизводственная среда протестирована, почему он не может работать онлайн.

одноклассникиК слову о причине: в интерфейсе отсутствует поле со значением, что приводит к ошибке.

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

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

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

Такая сцена не знает, если у вас есть? o (╥﹏╥) o.

проблема вызывает

Как показано ниже: Существует такая страница набора динамических форм после того, как пользователь выбирает или заполняет информацию (各字段非必填情况下也可以直接提交), затем передняя часть отправляет данные в заднюю часть и заканчивается, кажется, что у нее не так уж сложная логика.

image.png

Непосредственная причина ошибки

Если не требуется, поле signInfo будетJSON.stringifyПосле того, как строковый объект отсутствуетvaluekey, что приводит к тому, что внутренний синтаксический анализ не может правильно прочитать значение, а затем сообщает об исключении в систему интерфейса, и пользователь не может перейти к следующему шагу.

// 异常入参数据,数组字符串中没有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}]'

решение

Причина проблемы найдена и решение(Здесь упоминается только внешнее решение, конечно, оно также может быть решено бэкендом)Это также очень просто: преобразовать элемент, значение которого не определено, в пустую строку и отправить его.

Вариант 1. Откройте новый объект для обработки

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

Вариант 2: использоватьJSON.stringifyВторой параметр, непосредственно обрабатываемый

Недостаток схемы 1 в том, что для ее решения необходимо открыть новый объект и выполнить операцию по его решению.不够优雅

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, опционально заменяя значения, если указана функция заменителя, или опционально содержа только свойства, указанные массивом, если указанный заменитель является массивом.

Следующая информация взята изMDN

грамматика

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

параметр

  • value

    Значение, которое будет сериализовано в строку JSON.

  • replacerнеобязательный

    1. Если параметр является функцией, в процессе сериализации каждый атрибут сериализованного значения будет преобразован и обработан функцией;
    2. Если параметр является массивом, только имена свойств, содержащиеся в этом массиве, будут сериализованы в окончательную строку JSON;
    3. Если этот параметр имеет значение null или не указан, все свойства объекта будут сериализованы.
  • spaceнеобязательный

    1. Укажите пустую строку для отступа, используемого для украшения вывода (красивая печать);
    2. Если аргумент является числом, он представляет количество пробелов; верхний предел равен 10.
    3. Если значение меньше 1, значит пробелов нет;
    4. Если параметр является строкой (когда длина строки превышает 10 букв, берутся первые 10 букв), строка будет восприниматься как пробел;
    5. Если этот параметр не указан (или имеет значение null), пробелов не будет.

возвращаемое значение

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

аномальный

  • Исключение возникает при создании циклической ссылкиTypeError("циклическое значение объекта")
  • при попытке конвертироватьBigIntЗначение типа throwsTypeError("значение BigInt не может быть сериализовано в JSON") (значение BigInt не может быть сериализовано в JSON).

основное использование

Уведомление

  1. JSON.stringify может преобразовывать объекты или значения (обычно больше используется для преобразования объектов)
  2. можно указатьreplacerдополнительная замена функций
  3. можно также указатьreplacerпредставляет собой массив, который преобразует указанное свойство

вот толькоMDNпримерно на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值одеяло单独转换, возвращает неопределенное
// 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

Функция два

布尔值,数字,字符串Объект обертки автоматически преобразуется в исходное значение, соответствующее сериализации.

console.log(JSON.stringify([new Number(1), new String("前端胖头鱼"), new Boolean(false)]))
// '[1,"前端胖头鱼",false]'

Функция три

все сsymbolАтрибуты, являющиеся ключами атрибутов, полностью игнорируются, даже еслиreplacerОбязательно включать их в параметр.

console.log(JSON.stringify({
  [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(), чтобы преобразовать его в строковую строку (такую ​​же, как 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"

Функция семь

Выполнение этого метода для объектов, содержащих циклические ссылки (объекты, ссылающиеся друг на друга, образующие бесконечный цикл), вызовет ошибку.

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много функций! Давайте напишем простую версию, основанную на этих функциях (Нет функции замены и пространства)

реализация исходного кода

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(() => { })) // 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

Из приведенного выше теста видно, чтоjsonstringifyосновные иJSON.stringifyПроизводительность стабильна (также возможно, что тестовые примеры недостаточно полны, добро пожаловать на совместное обучение)

конец

Из-за бага заново учусьJSON.stringify, я узнал, что в нем все еще так много функций, которые я обычно не замечаю. Круг внешних развлечений слишком глубок. Я надеюсь, что со всеми будут обращаться мягко, с меньшим количеством ошибок и большим вниманием. Спокойной ночи