JSON.parse 与 JSON.stringify 可能引发的问题

引言

在现代Web开发中,JSON (JavaScript Object Notation) 已成为数据交换的事实标准。它轻量、易读、易于解析,并被各种编程语言广泛支持。JavaScript 内置的 JSON.stringify()JSON.parse() 方法,为我们提供了在对象和字符串之间进行序列化和反序列化的便捷工具。

然而,尽管这两个方法功能强大且使用频繁,但它们并非万能。在处理复杂的JavaScript对象时,开发者常常会遇到一些意料之外的行为,导致数据丢失、类型错误甚至程序崩溃。

这些"陷阱"源于JSON规范本身的定义以及 JSON.stringify 的实现机制。

笔者在最近做的一些事情中频繁使用了这些能力,同事观测到了一些错误,所以想着在本次文章中详细聊聊JSON.parseJSON.stringify的限制。

核心限制场景分析

1. 不支持的数据类型

JSON的规范只定义了六种数据类型:string, number, object, array, boolean, null。任何不在此列的JavaScript原生类型,在序列化过程中都会被特殊处理或直接忽略。

  • 问题分类: 不支持的数据类型
  • 具体表现与代码示例:
类型 示例代码 JSON.stringify 结果
undefined JSON.stringify({ a: undefined }) "{}" (属性被忽略)
JSON.stringify([1, undefined, 2]) "[1,null,2]" (值变为 null)
JSON.stringify(undefined) undefined (非字符串)
Function JSON.stringify({ f: () => {} }) "{}" (属性被忽略)
JSON.stringify([1, () => {}, 2]) "[1,null,2]" (值变为 null)
Symbol JSON.stringify({ s: Symbol('id') }) "{}" (属性被忽略)
JSON.stringify([1, Symbol('id'), 2]) "[1,null,2]" (值变为 null)
BigInt JSON.stringify({ n: 123n }) TypeError (直接抛出错误)
ts 复制代码
// 示例
const data = {
  // 以下属性将在序列化时丢失
  undef: undefined,
  func: () => console.log('hello'),
  sym: Symbol('unique'),
  // BigInt 会直接导致错误
  // big: 1234567890123456789012345678901234567890n
};

console.log(JSON.stringify(data)); // 输出: "{}"

const arr = [undefined, () => {}, Symbol('id')];
console.log(JSON.stringify(arr)); // 输出: "[null,null,null]"

try {
  JSON.stringify({ big: 123n });
} catch (error) {
  console.log(error.name); // 输出: "TypeError"
}
  • 原因分析:

    • undefined, Function, Symbol 不是JSON规范的一部分。ECMA-262标准规定,在序列化对象时,如果属性值为这些类型,则该属性会被忽略。在数组中,它们会被转换为 null,以保持数组的长度和元素位置。
    • BigInt 类型因为无法在不损失精度的情况下安全地转换为JSON支持的 Number 类型,所以规范选择直接抛出 TypeError 以提醒开发者进行显式处理。
  • 替代解决方案: 使用 replacer 函数来自定义序列化过程,将不支持的类型转换为JSON兼容的格式。

ts 复制代码
const dataWithBigInt = {
  id: 1,
  value: 12345678901234567890n,
  action: () => {} // 函数依然会被忽略,除非在replacer中处理
};

const replacer = (key, value) => {
  if (typeof value === 'bigint') {
    // 将 BigInt 转换为带特殊标记的字符串
    return `BIGINT::${value.toString()}`;
  }
  if (typeof value === 'undefined') {
    return 'UNDEFINED::';
  }
  // 其他类型保持默认行为
  return value;
};

const jsonString = JSON.stringify(dataWithBigInt, replacer);
console.log(jsonString); // 输出: '{"id":1,"value":"BIGINT::12345678901234567890"}'

// 使用 reviver 函数进行反序列化
const reviver = (key, value) => {
  if (typeof value === 'string' && value.startsWith('BIGINT::')) {
    return BigInt(value.substring(8));
  }
  if (value === 'UNDEFINED::') {
    return undefined;
  }
  return value;
};

const parsedData = JSON.parse(jsonString, reviver);
console.log(parsedData.value); // 输出: 12345678901234567890n
console.log(typeof parsedData.value); // 输出: "bigint"

2. 特殊数值的处理

JavaScript中的一些特殊数值,如 NaN, Infinity-Infinity,在JSON中也没有对应的表示。

  • 问题分类: 特殊数值处理
  • 具体表现与代码示例:
ts 复制代码
const data = {
  notANumber: NaN,
  positiveInfinity: Infinity,
  negativeInfinity: -Infinity
};

// 在对象或数组中,这些值都会被序列化为 null
const jsonString = JSON.stringify(data);
console.log(jsonString); // 输出: '{"notANumber":null,"positiveInfinity":null,"negativeInfinity":null}'
  • 原因分析: JSON的 number 类型规范基于IEEE 754双精度浮点数,但明确排除了非有限值(non-finite values)。因此,JSON.stringify 将这些JavaScript中的特殊数值转换为 null
  • 替代解决方案: 与处理 BigInt 类似,使用 replacerreviver 函数进行特殊标记和转换。
ts 复制代码
const data = { value: NaN };

const replacer = (key, value) => {
  if (Number.isNaN(value)) return 'NaN';
  if (value === Infinity) return 'Infinity';
  if (value === -Infinity) return '-Infinity';
  return value;
};

const jsonString = JSON.stringify(data, replacer);
console.log(jsonString); // 输出: '{"value":"NaN"}'

const reviver = (key, value) => {
  if (value === 'NaN') return NaN;
  if (value === 'Infinity') return Infinity;
  if (value === '-Infinity') return -Infinity;
  return value;
};

const parsedData = JSON.parse(jsonString, reviver);
console.log(parsedData.value); // 输出: NaN

3. Date 对象的序列化与反序列化

Date 对象在序列化时有特殊的自动转换行为,但反序列化时却无法自动还原。

  • 问题分类: Date 对象的处理
  • 具体表现与代码示例:
ts 复制代码
const data = {
  event: 'Meeting',
  time: new Date()
};

const jsonString = JSON.stringify(data);
console.log(jsonString); // 输出类似: '{"event":"Meeting","time":"2025-08-02T10:00:00.000Z"}'

const parsedData = JSON.parse(jsonString);
console.log(typeof parsedData.time); // 输出: "string"
// parsedData.time.getFullYear(); // 这会抛出 TypeError,因为它是一个字符串
  • 原因分析:JSON.stringify 遇到一个具有 toJSON 方法的对象时,它会调用该方法并序列化其返回值。Date.prototype.toJSON 方法会返回一个符合 ISO 8601 格式的日期字符串。然而,JSON规范本身没有日期类型,所以 JSON.parse 在解析时,只会将这个字符串视为一个普通的字符串,而不会自动将其转换回 Date 对象。
  • 替代解决方案: 使用 reviver 函数在解析时检查字符串格式,并将其手动转换回 Date 对象。
ts 复制代码
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;

const reviver = (key, value) => {
  if (typeof value === 'string' && isoDateRegex.test(value)) {
    return new Date(value);
  }
  return value;
};

const jsonString = '{"time":"2025-08-02T10:00:00.000Z"}';
const parsedData = JSON.parse(jsonString, reviver);

console.log(parsedData.time instanceof Date); // 输出: true
console.log(parsedData.time.getFullYear()); // 输出: 2025

4. 循环引用

当一个对象的属性直接或间接地引用了自身,就形成了循环引用。尝试序列化这样的对象会失败。

  • 问题分类: 循环引用 (Circular References)
  • 具体表现与代码示例:
ts 复制代码
const circularObject = {};
circularObject.self = circularObject;

try {
  JSON.stringify(circularObject);
} catch (error) {
  console.log(error.name); // 输出: "TypeError"
  console.log(error.message); // 输出类似: "Converting circular structure to JSON"
}
  • 原因分析: 序列化算法在遍历对象属性时会检测已经访问过的对象。如果再次遇到同一个对象,它会判断存在循环引用,并抛出 TypeError 以防止无限递归导致的堆栈溢出。

  • 替代解决方案:

    • 自定义 replacer 函数: 手动处理循环引用,将其替换为一个占位符。

      ts 复制代码
         const circularObject = { name: 'A' };
         const otherObject = { name: 'B', parent: circularObject };
         circularObject.child = otherObject;
      
         const getCircularReplacer = () => {
           const seen = new WeakSet();
           return (key, value) => {
             if (typeof value === 'object' && value !== null) {
               if (seen.has(value)) {
                 return '[Circular]'; // 用占位符替换循环引用
               }
               seen.add(value);
             }
             return value;
           };
         };
      
         const jsonString = JSON.stringify(circularObject, getCircularReplacer());
         console.log(jsonString); // 输出: '{"name":"A","child":{"name":"B","parent":"[Circular]"}}'
    • 使用第三方库: 对于复杂的场景,使用专门处理循环引用的库会更简单可靠,例如 flatted

      ts 复制代码
          // 需要先安装:npm install flatted
          // import { stringify, parse } from 'flatted'; // 在 Node.js 或模块化项目中
          // 或者直接在浏览器中引入 flatted.js
      
          const { stringify, parse } = flatted; // 假设已引入
      
          const a = {};
          a.b = a;
      
          const jsonString = stringify(a);
          console.log(jsonString); // 输出: '["1",{"b":"0"}]' (flatted 的特殊格式)
      
          const parsedObject = parse(jsonString);
          console.log(parsedObject.b === parsedObject); // 输出: true

5. JSON.parse 的语法错误

JSON.stringify 在处理不兼容的 JavaScript 数据结构 时抛出 TypeError 不同,JSON.parse 的错误主要源于输入 字符串 不符合严格的JSON语法规范,此时它会抛出 SyntaxError

  • 问题分类: 不符合JSON语法的输入
  1. 未转义的特殊/控制字符: JSON字符串中的换行符、制表符等必须被转义。
ts 复制代码
const invalidJsonString = '{"key":"value\n"}'; // 字符串中包含一个未转义的换行符

try {
    JSON.parse(invalidJsonString);
} catch (error) {
    // 明确的报错表现
    console.log(error);
    // 输出: SyntaxError: Unexpected token in JSON at position 13 (或类似信息)
}

修正方法: 正确转义控制字符,例如 \n 应写为 \n

  1. 无效的 Unicode 序列: JSON规范要求 \u 后面必须跟随4位十六进制数字。
ts 复制代码
const invalidUnicodeString = '"\u002"'; // \u 后面只有3位数字

try {
    JSON.parse(invalidUnicodeString);
} catch (error) {
    // 明确的报错表现
    console.log(error);
    // 输出: SyntaxError: Bad Unicode escape in JSON at position 5
}

修正方法: 补全为4位十六进制数字,例如 \u0020

  1. 属性名未使用双引号: JSON要求对象的键(属性名)必须用双引号包裹。
ts 复制代码
const singleQuoteKey = "{'key':'value'}"; // 使用了单引号
const noQuoteKey = "{key:'value'}";     // 没有使用引号

try {
   JSON.parse(noQuoteKey);
} catch (error) {
   // 明确的报错表现
   console.log(error);
   // 输出: SyntaxError: Unexpected token k in JSON at position 1
}
  1. 使用单引号代替双引号: JSON规范只接受双引号作为字符串的分隔符。
ts 复制代码
const singleQuoteValue = '{"key":'value'}'; // 值使用了单引号

try {
    JSON.parse(singleQuoteValue);
} catch (error) {
    // 明确的报错表现
    console.log(error);
    // 输出: SyntaxError: Unexpected token ' in JSON at position 7
}
  1. 末尾有多余的逗号 (Trailing Comma): JSON规范不允许在数组或对象的最后一个元素后有多余的逗号。
ts 复制代码
const trailingComma = '{"key":"value",}';

try {
    JSON.parse(trailingComma);
} catch (error) {
    // 明确的报错表现
    console.log(error);
    // 输出: SyntaxError: Unexpected token } in JSON at position 15
}
  • 原因分析: JSON.parse 遵循的是非常严格的JSON语法标准(RFC 8259)。这个标准比JavaScript的对象字面量语法要严格得多。任何偏离该标准的写法,例如单引号、未引用的键、末尾逗号或无效的转义序列,都会被解析器视为语法错误,从而中断解析并抛出 SyntaxError

  • 替代解决方案:

    • 确保来源可靠: 确保生成JSON字符串的服务端或源头严格遵守JSON规范。

    • 预处理 字符串: 在解析之前,可以对一些常见的非标准JSON进行预处理。但这通常是"最后的手段",因为容易引入新的问题。

ts 复制代码
// 示例:一个非常脆弱的、仅用于演示的修正函数
function sanitizeJsonString(str) {
    // 尝试将单引号替换为双引号 (非常不推荐,可能破坏字符串内容)
    return str.replace(/'/g, '"');
}
// 更稳妥的方式是使用专门处理非标准JSON的库,如 `json5`。

总结

JSON.stringifyJSON.parse 是JavaScript中不可或缺的工具,但它们的行为严格受限于JSON规范。

  1. 数据类型不完整: undefined, Function, Symbol 等类型会被忽略或转为 null,而 BigInt 会直接报错。
  2. 特殊值丢失: NaNInfinity 会被序列化为 null,导致数据精度丢失。
  3. 类型信息丢失: Date 对象会被转换为字符串,且无法自动复原。
  4. 结构限制: 无法处理循环引用的对象结构。

为了应对这些挑战,replacer reviver 函数是我们的首选武器。通过这两个钩子函数,我们可以精确地控制序列化和反序列化的过程,自定义处理各种特殊情况,从而实现无损的数据交换。

对于更复杂的场景,如深度的循环引用或需要保持完整的类型信息,引入成熟的第三方库(如 flatted, superjson)会更保险一些。。

那么这就是本篇文章的全部内容啦~

相关推荐
小喷友2 分钟前
第 6 章:API 路由(后端能力)
前端·react.js·next.js
像素之间5 分钟前
elementui中rules的validator 用法
前端·javascript·elementui
小高0079 分钟前
🚀把 async/await 拆成 4 块乐高!面试官当场鼓掌👏
前端·javascript·面试
CF14年老兵10 分钟前
SQL 是什么?初学者完全指南
前端·后端·sql
2401_8370885014 分钟前
AJAX快速入门 - 四个核心步骤
前端·javascript·ajax
一月是个猫20 分钟前
前端工程化之Lint工具链
前端
小潘同学20 分钟前
less 和 sass的区别
前端
无羡仙21 分钟前
当点击链接不再刷新页面
前端·javascript·html
王小发10121 分钟前
快速知道 canvas 来进行微信网页视频无限循环播放的思路
前端
雲墨款哥22 分钟前
为什么我的this.name输出了空字符串?严格模式与作用域链的微妙关系
前端·javascript·面试