Appearance
JSON 序列化:不只是一个 API
JSON.stringify 和 JSON.parse 在工程中远远不是"传个对象就能用"。函数处理、循环引用、BigInt 报错、Date 自动转换——这些行为在不同场景下有完全不同的应对策略。
JSON.stringify 在 V8 中的执行路径
text
JSON.stringify(value)
→ C++ JsonStringifier::SerializeJSObject(value)
→ 序列化为 std::vector<uint8_t>
→ 转回 v8::String
→ 返回 JS 字符串JsonStringifier 内部维护两个关键状态:
- Stack(访问栈):用于循环引用检测。进入一个对象时 push,离开时 pop。如果在栈中发现同一个对象引用,抛出
TypeError: Converting circular structure to JSON。 - PropertyList:对于定义了
toJSON()方法的对象,只序列化toJSON()返回的值。
六种特殊类型的处理
| 值 | JSON.stringify 行为 | 原因 |
|---|---|---|
undefined | 属性被跳过;数组中转 null | JSON 格式无 undefined 类型 |
Function | 属性被跳过 | JSON 不支持函数 |
Symbol | 属性被跳过 | JSON 不支持 Symbol |
BigInt | 抛出 TypeError | 没有安全的数字表示(>2^53 精度丢失) |
NaN / Infinity | 转 null | JSON 规范不允许 NaN/Infinity |
Date | 调用 toISOString() | 通过 toJSON() 方法 |
Map / Set / WeakMap / WeakSet | 转 {} | 内部属性为不可枚举,stringify 只处理可枚举自有属性 |
| 循环引用 | 抛出 TypeError | Stack 去重检测 |
这些行为是 JSON 格式设计本身决定的,不是 JSON.stringify 的实现缺陷。JSON 作为跨语言数据交换格式,不支持 JS 特有的类型。
toJSON 方法的调用时机
JSON.stringify 遇到一个对象时,会先检查它是否有 toJSON 方法。如果有,调用 toJSON() 并用返回值替代原对象继续序列化:
js
const obj = {
name: 'event',
createdAt: new Date(),
toJSON() {
return { name: this.name, time: this.createdAt.getTime() };
}
};
JSON.stringify(obj);
// '{"name":"event","time":1700000000000}'Date.prototype.toJSON 是内置的——它等价于 Date.prototype.toISOString()。这也是为什么 Date 在 JSON.stringify 中自动转为 ISO 字符串。
自定义 toJSON 的主要用途:排除不可序列化的字段、转换类型(如 Date → timestamp)、或调整输出结构以匹配外部系统的 schema。
JSON.parse reviver 的安全风险
JSON.parse 的第二个参数 reviver 允许在解析过程中转换值:
js
// ❌ 危险:用 eval 恢复函数
JSON.parse(str, (k, v) => {
if (typeof v === 'string' && v.startsWith('function')) {
return eval(`(${v})`);
}
return v;
});eval 执行的是任意 JS 代码。如果 JSON 字符串来自用户输入、URL 参数、或任何不可信来源,这等于一个 RCE 漏洞。
更安全的替代方案:不要序列化函数。 如果数据模型中包含函数,把数据和行为分开——这是更健康的设计。
如果确实需要跨上下文传递函数逻辑,考虑:
- 传递函数标识符(函数名),在消费方查表调用
- 使用 Web Worker 的
postMessage+ 预定义的消息 type - 使用沙箱化的
new Function()(比eval稍安全,但在 CSP 环境下不可用)
structuredClone:现代替代方案
structuredClone(2022,所有现代浏览器 + Node 17+)是深拷贝和序列化的首选:
js
const obj = { a: 1, b: { c: 2 } };
const cloned = structuredClone(obj);
// 深拷贝,支持循环引用structuredClone 相比 JSON.parse(JSON.stringify()) 的优势:
| 特性 | JSON | structuredClone |
|---|---|---|
| 循环引用 | ❌ 报错 | ✅ 正确复制 |
| Date | ✅(转 ISO string) | ✅(保持 Date 对象) |
| Map / Set | ❌ 转 {} | ✅ 完整支持 |
| RegExp | ❌ 转 {} | ✅ 完整支持 |
| ArrayBuffer / TypedArray | ❌ | ✅ 完整支持 |
| Function | ❌ 跳过 | ❌ 报错 DataCloneError |
| Symbol | ❌ 跳过 | ❌ 报错 DataCloneError |
| DOM 节点 | ❌ 跳过 | ❌ 报错 DataCloneError |
| Error 对象 | ❌ 转 {} | ✅ 完整支持 |
性能方面,structuredClone 对大型对象(>100KB)明显快于 JSON.parse(JSON.stringify()),因为它直接操作引擎内部的序列化格式,不需要经过字符串中间表示。
structuredClone 的局限性是它使用结构化克隆算法(和 postMessage 相同),不支持函数、Symbol、DOM 节点。这不是缺陷,是算法设计目标决定的——它设计用于跨上下文传递数据,而不是代码。
生产场景的序列化策略
| 场景 | 推荐方案 |
|---|---|
| API 请求/响应 | JSON.stringify + JSON.parse |
| 深拷贝纯数据 | structuredClone |
| Web Worker 消息 | postMessage(自动 structured clone) |
| 持久化到 IndexedDB | structuredClone(部分浏览器支持直接存储) |
| 包含 Date 的拷贝 | structuredClone |
| 包含循环引用的拷贝 | structuredClone 或 lodash.cloneDeep |
| 包含函数的对象 | 不要序列化函数——重构为数据+行为分离 |
| 大对象(>1MB)序列化 | structuredClone(避免字符串中间态) |
| 浏览器不支持 structuredClone | MessageChannel postMessage hack 或 lodash.cloneDeep |
