引言
在现代Web开发中,JSON (JavaScript Object Notation) 已成为数据交换的事实标准。它轻量、易读、易于解析,并被各种编程语言广泛支持。JavaScript 内置的 JSON.stringify()
和 JSON.parse()
方法,为我们提供了在对象和字符串之间进行序列化和反序列化的便捷工具。
然而,尽管这两个方法功能强大且使用频繁,但它们并非万能。在处理复杂的JavaScript对象时,开发者常常会遇到一些意料之外的行为,导致数据丢失、类型错误甚至程序崩溃。
这些"陷阱"源于JSON规范本身的定义以及 JSON.stringify
的实现机制。
笔者在最近做的一些事情中频繁使用了这些能力,同事观测到了一些错误,所以想着在本次文章中详细聊聊JSON.parse
与 JSON.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
类似,使用replacer
和reviver
函数进行特殊标记和转换。
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
函数: 手动处理循环引用,将其替换为一个占位符。tsconst 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语法的输入
- 未转义的特殊/控制字符: 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
。
- 无效的 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
。
- 属性名未使用双引号: 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
}
- 使用单引号代替双引号: JSON规范只接受双引号作为字符串的分隔符。
ts
const singleQuoteValue = '{"key":'value'}'; // 值使用了单引号
try {
JSON.parse(singleQuoteValue);
} catch (error) {
// 明确的报错表现
console.log(error);
// 输出: SyntaxError: Unexpected token ' in JSON at position 7
}
- 末尾有多余的逗号 (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.stringify
和 JSON.parse
是JavaScript中不可或缺的工具,但它们的行为严格受限于JSON规范。
- 数据类型不完整:
undefined
,Function
,Symbol
等类型会被忽略或转为null
,而BigInt
会直接报错。 - 特殊值丢失:
NaN
和Infinity
会被序列化为null
,导致数据精度丢失。 - 类型信息丢失:
Date
对象会被转换为字符串,且无法自动复原。 - 结构限制: 无法处理循环引用的对象结构。
为了应对这些挑战,replacer
和 reviver
函数是我们的首选武器。通过这两个钩子函数,我们可以精确地控制序列化和反序列化的过程,自定义处理各种特殊情况,从而实现无损的数据交换。
对于更复杂的场景,如深度的循环引用或需要保持完整的类型信息,引入成熟的第三方库(如 flatted
, superjson
)会更保险一些。。
那么这就是本篇文章的全部内容啦~