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 参数传入数字表示缩进空格数,传入字符串如 "--" 则会用该字符串作为缩进。当 space 为 0 或空字符串时不换行;为 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 函数
reviver 与 replacer 对称,在反序列化时对每对键值进行后处理,是实现类型还原(如日期字符串转 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 数据的加载代码更加简洁。