Skip to content

JSON 序列化:不只是一个 API

JSON.stringifyJSON.parse 在工程中远远不是"传个对象就能用"。函数处理、循环引用、BigInt 报错、Date 自动转换——这些行为在不同场景下有完全不同的应对策略。


JSON.stringify 在 V8 中的执行路径

text
JSON.stringify(value)
  → C++ JsonStringifier::SerializeJSObject(value)
  → 序列化为 std::vector<uint8_t>
  → 转回 v8::String
  → 返回 JS 字符串

JsonStringifier 内部维护两个关键状态:

  1. Stack(访问栈):用于循环引用检测。进入一个对象时 push,离开时 pop。如果在栈中发现同一个对象引用,抛出 TypeError: Converting circular structure to JSON
  2. PropertyList:对于定义了 toJSON() 方法的对象,只序列化 toJSON() 返回的值。

六种特殊类型的处理

JSON.stringify 行为原因
undefined属性被跳过;数组中转 nullJSON 格式无 undefined 类型
Function属性被跳过JSON 不支持函数
Symbol属性被跳过JSON 不支持 Symbol
BigInt抛出 TypeError没有安全的数字表示(>2^53 精度丢失)
NaN / InfinitynullJSON 规范不允许 NaN/Infinity
Date调用 toISOString()通过 toJSON() 方法
Map / Set / WeakMap / WeakSet{}内部属性为不可枚举,stringify 只处理可枚举自有属性
循环引用抛出 TypeErrorStack 去重检测

这些行为是 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()) 的优势:

特性JSONstructuredClone
循环引用❌ 报错✅ 正确复制
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)
持久化到 IndexedDBstructuredClone(部分浏览器支持直接存储)
包含 Date 的拷贝structuredClone
包含循环引用的拷贝structuredClone 或 lodash.cloneDeep
包含函数的对象不要序列化函数——重构为数据+行为分离
大对象(>1MB)序列化structuredClone(避免字符串中间态)
浏览器不支持 structuredCloneMessageChannel postMessage hack 或 lodash.cloneDeep