JavaScript JSON 序列化完全指南

JavaScript 阅读约 13 分钟
JavaScriptJSON序列化JSON.stringifyJSON.parse

深入解析 JavaScript 中 JSON.stringify() 与 JSON.parse() 的全部特性,掌握序列化规则、常见陷阱及第三方工具库的高级用法。

JSON.stringify() 核心参数

JSON.stringify() 是 JavaScript 中将值转换为 JSON 字符串的标准方法。它接收三个参数:要序列化的值、replacer(过滤或转换函数/数组)、以及 space(控制缩进的字符串或数字)。三个参数的合理组合可以覆盖从 API 数据清洗到日志美化输出的绝大部分场景。

const user = {
  name: "张三",
  password: "secret123",
  email: "zhangsan@example.com",
  role: "admin",
  metadata: { loginCount: 42, lastIp: "192.168.1.1" },
};

// replacer 作为数组:只输出白名单中的键(浅层过滤)
const safe = JSON.stringify(user, ["name", "email", "metadata"], 2);
console.log(safe);
// {
//   "name": "张三",
//   "email": "zhangsan@example.com",
//   "metadata": {
//     "loginCount": 42,
//     "lastIp": "192.168.1.1"
//   }
// }
// 注意:白名单对嵌套对象不生效,metadata 内部全部输出

replacer 作为数组时只做第一层的白名单过滤,嵌套对象的属性不会被进一步筛选。space 参数传入数字表示缩进空格数,传入字符串如 "--" 则会用该字符串作为缩进。当 space0 或空字符串时不换行;为 null 或省略时输出紧凑格式。

replacer 函数的高级用法

当数组形式的白名单不够灵活时,函数形式的 replacer 提供了完全的键值控制能力。

const result = JSON.stringify(user, (key, value) => {
  // 脱敏密码和 IP 地址
  if (key === "password") return undefined;  // undefined 会删除该键
  if (key === "lastIp") return "***.***.***.***";
  // 对 admin 角色的显示进行降级处理
  if (key === "role" && value === "admin") return "***";
  return value;  // 其他值原样返回
}, 2);

console.log(result);
// {
//   "name": "张三",
//   "email": "zhangsan@example.com",
//   "role": "***",
//   "metadata": {
//     "loginCount": 42,
//     "lastIp": "***.***.***.***"
//   }
// }

replacer 函数的执行顺序遵循深度优先、由外到内的遍历规则。第一次调用时 key 为空字符串 ""value 是根对象本身。如果根调用返回 undefined,整个序列化结果为 undefined(而非字符串)。这个特性可以用来在序列化之前就拒绝某种类型的顶层值。

JSON.parse() 与 reviver 函数

reviverreplacer 对称,在反序列化时对每对键值进行后处理,是实现类型还原(如日期字符串转 Date 对象)的标准方式。

const json = '{"name":"张三","birth":"1995-07-15T00:00:00Z","score":"95"}';

const parsed = JSON.parse(json, (key, value) => {
  // 将 ISO 日期字符串还原为 Date 对象
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    const date = new Date(value);
    return isNaN(date.getTime()) ? value : date;  // 降级保留原字符串
  }
  // 数字字符串转 number(精确匹配才转换)
  if (key === "score" && typeof value === "string" && /^\d+$/.test(value)) {
    return Number(value);
  }
  return value;
});

console.log(parsed.birth instanceof Date); // true
console.log(typeof parsed.score);          // "number"

reviver 返回 undefined 会从结果对象中删除该键,这和 stringify 中的行为是对称的。如果需要保留键但赋值为 null,应该显式返回 null 而非 undefined。一个常见的坑是 reviver 对数组元素也逐一遍历,此时 key 是数组索引,需要注意区分。

toJSON() 方法的原理与实践

如果一个对象定义了 toJSON() 方法,JSON.stringify() 会自动调用它并直接序列化返回值,而不再遍历对象原本的属性。这个机制为对象提供了自定义序列化表示的能力。

const event = {
  title: "技术分享会",
  date: new Date("2026-07-20T09:00:00Z"),
  organizer: { name: "李四", department: "前端组" },
  toJSON() {
    // 扁平化输出,隐藏内部结构
    return {
      title: this.title,
      date: this.date.toISOString(),
      organizer: this.organizer.name,
    };
  },
};

console.log(JSON.stringify(event, null, 2));
// {
//   "title": "技术分享会",
//   "date": "2026-07-20T09:00:00.000Z",
//   "organizer": "李四"
// }

Date.prototype.toJSON() 返回 toISOString() 的结果,因此 JavaScript 的日期对象天然支持 JSON 序列化。自定义 toJSON() 最常见的使用场景包括:扁平化复杂嵌套结构以生成 API 友好格式、隐藏机密内部字段、以及为不支持 JSON 原生的类型(如自定义类的实例)提供序列化适配。需要注意的是 toJSON() 返回的值本身仍会递归调用 stringify,因此可以返回任意可序列化的值。

序列化规则详解

JSON.stringify() 对不同 JavaScript 类型的处理规则非常严格,理解这些规则是避免生产环境数据丢失的关键。

const data = {
  undef: undefined,        // 属性中 → 忽略(不输出)
  sym: Symbol("id"),       // 属性中 → 忽略
  fn: function() {},       // 属性中 → 忽略
  bigint: BigInt(42),      // 直接抛 TypeError!
  nan: NaN,                // → 转为 null(有损转换)
  infinity: Infinity,      // → 转为 null(有损转换)
  date: new Date(),        // → 调用 toISOString()
  arr: [undefined, Symbol("x"), function() {}],
  // 数组中 → 全部转为 null
};

try {
  console.log(JSON.stringify(data));
} catch (e) {
  console.error("BigInt 不能序列化:", e.message);
}

BigInt 是 ES2020 引入的类型,JSON 规范中并没有对应的表示,因此直接序列化会抛出 TypeError。解决方案包括:通过 replacer 调用 BigInt.prototype.toString() 输出为字符串,或序列化前将 BigInt 属性转为普通数字(但要注意精度风险)。NaN 和 Infinity 转为 null 属于有损转换,如果你的数据依赖于这些特殊值,必须在序列化前显式处理为约定好的字符串或数值哨兵。

深度克隆的局限性

JSON.parse(JSON.stringify(obj)) 是 JavaScript 中流传最广的深拷贝技巧,但它存在多项不可逆的数据丢失问题。

const original = {
  date: new Date(),
  fn: () => "hello",
  re: /test/gi,
  undef: undefined,
  map: new Map([["key", "value"]]),
  set: new Set([1, 2, 3]),
  nested: { value: 42 },
  [Symbol("unique")]: "lost",
};

const cloned = JSON.parse(JSON.stringify(original));

console.log(cloned.date);  // 字符串,不再是的 Date 实例
console.log(cloned.fn);    // undefined
console.log(cloned.re);    // {}(空对象,修饰符丢失)
console.log(cloned.map);   // {}(空对象,键值对丢失)
console.log(cloned.set);   // {}(空对象,元素丢失)
// Symbol 键的属性直接不存在

日期变字符串、正则退化为空对象、函数和 Symbol 彻底丢失、Map 和 Set 被转为空普通对象。这种深拷贝方式仅适用于仅包含纯 JSON 原始类型(string、number、boolean、null、object、array)的简单数据字典。现代 JavaScript 中推荐使用 structuredClone() 进行深拷贝,它支持 Date、Map、Set、ArrayBuffer、RegExp 等多种内置类型,但不支持函数和 Symbol。

常见陷阱与第三方库方案

循环引用是 JSON.stringify() 的天敌,会直接抛出 TypeError。对于需要序列化复杂对象图的场景,社区提供了专门的解决方案。

// 使用 flatted 处理循环引用
import { stringify, parse } from "flatted";

const a = { name: "A" };
const b = { name: "B", ref: a };
a.ref = b;
// JSON.stringify(a);  // TypeError: Converting circular structure to JSON

const safe = stringify(a);
console.log(safe);  // 包含特殊标记的正常 JSON 字符串
const restored = parse(safe);
console.log(restored.ref.ref === restored);  // true

对于需要跨语言互操作或更高保真度的场景,superjson 提供了更完善的支持。

import superjson from "superjson";

const data = {
  created: new Date(),
  counters: new Map([["a", 1], ["b", 2]]),
  id: BigInt("12345678901234567890"),
  pattern: /hello/gi,
};

const serialized = superjson.stringify(data);
const deserialized = superjson.parse(serialized);

console.log(deserialized.created instanceof Date);  // true
console.log(deserialized.counters instanceof Map);   // true
console.log(typeof deserialized.id);                 // "bigint"
console.log(deserialized.pattern instanceof RegExp); // true

superjson 支持 Date、Map、Set、BigInt、RegExp、URL 等多种原生类型的无损往返。它通过在 JSON 中嵌入类型标记({"json": ..., "meta": ...} 结构)来实现,代价是输出体积略大于原始 JSON,且非标准格式需要双方都使用同一个库。

浏览器与 Node.js 差异

浏览器和 Node.js 中的 JSON 全局对象都基于 ECMAScript 规范实现,核心 API 行为完全一致。但运行环境的不同带来了实践层面的几个细微差异:

  • 主线程阻塞:浏览器的 JSON.stringify() 在主线程上同步执行。序列化包含百万级元素的数组可能花费数百毫秒,在此期间 UI 完全冻结。解决方案是将序列化任务交给 Web Worker 处理,或者分批(chunk)序列化。
  • 内存上限:Node.js(V8 引擎)的字符串最大长度受堆内存限制,大约 512MB 到 1GB 之间。超过此限制的 JSON 应使用流式处理。
  • require JSON:Node.js 的 require() 可以直接导入 .json 文件并自动解析为 JavaScript 对象,且结果会被缓存。浏览器则需要显式调用 fetch().json()
  • 顶层 await:Node.js 14+ 和现代浏览器均支持在模块中直接 await fetch().then(r => r.json()),使 JSON 数据的加载代码更加简洁。

各语言 JSON API 对比

语言 序列化 反序列化 美化输出 配置方式
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize<T>(str) WriteIndented = true JsonSerializerOptions

常见问题 (FAQ)

JSON 序列化时如何处理日期时间类型?

大多数语言的 JSON 库默认不支持日期时间类型。通常做法是序列化为 ISO 8601 格式字符串(如 "2026-07-15T10:30:00Z")或 Unix 时间戳,反序列化时再转换回日期时间对象。Python 可通过 default 参数指定自定义编码器;JavaScript 可在 toJSON 方法中处理;Java 的 Jackson 支持 @JsonFormat 注解配置日期格式。

如何忽略 JSON 序列化中的 null 值或空字段?

不同语言的实现方式不同:Python 可在自定义编码器中过滤 None 值;JavaScript 可使用 replacer 函数或 JSON.stringify 的第二个参数;Java Jackson 用 @JsonInclude(Include.NON_NULL) 注解;Go 使用 omitempty struct tag;C# 设置 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull。

JSON 序列化时如何处理循环引用?

循环引用是 JSON 序列化的常见陷阱。标准 JSON 库通常会抛出异常或栈溢出。解决方案包括:使用 @JsonIdentityInfo 注解(Java Jackson)、实现自定义序列化器跳过反向引用、将对象图转换为 DTO 后再序列化,或使用专门的图序列化库如 flatted(JavaScript)。

为什么我的私有字段没有被序列化?

JSON 序列化库通常只序列化公开(public)字段或带有 getter 的属性。这是设计上的安全考虑。Python 的 json 模块默认只序列化 dict 的公共键;Java Jackson 可通过 @JsonProperty 注解私有字段或配置 ObjectMapper 的 Visibility;C# 中 System.Text.Json 默认只序列化公共属性,需通过 [JsonInclude] 特性包含非公共成员。

JSON 序列化性能有哪些优化技巧?

1) 复用序列化器实例(避免每次创建 ObjectMapper/JsonSerializerOptions);2) 对于大型数据,使用流式 API(如 Jackson 的 JsonGenerator / Python 的 iterencode);3) 使用编译时生成(C# Source Generator、Go easyjson);4) 避免过多嵌套层次;5) 数值类型避免不必要的大精度;6) 使用数据库分页或分块传输大 JSON。

推荐工具

以下工具可以帮助你在 JavaScript 开发中更高效地处理 JSON 数据:

相关文章