JSON.stringify 与 JSON.parse 的 replacer、reviver 能做什么
JSON.stringify() 和 JSON.parse() 看起来只有"转字符串"和"还原对象"两个动作,但它们的第二、第三个参数(replacer、space、reviver)决定了序列化过程中每个字段的去留、改写和还原方式。常见的真实需求是:发给 API 或写入本地存储前剔除密码等敏感字段、把解析回来的日期字符串变回 Date 对象、保留 Map/Set/BigInt、以及让带循环引用的对象不再抛出 TypeError。33-js-concepts 项目的 JSON Deep Dive 文档完整覆盖了这些用法,并且仓库里有一份对应测试 json-deep-dive.test.js可以把本文所有行为固化为断言。以下代码均可直接在 Node 或浏览器控制台中运行,预期输出以文档标注为准。
先明确三个参数各自的位置和调用约定
JSON.stringify(value)
JSON.stringify(value, replacer)
JSON.stringify(value, replacer, space)
JSON.parse(text)
JSON.parse(text, reviver)
两者的回调签名相同,但语义不同:
- replacer(stringify 第二参数):对对象树中每个键值对调用。
this是包含当前属性的对象,key是属性名(数组中为下标),value是属性值。返回要写入输出的值,返回undefined则该属性被排除。 - reviver(parse 第二参数):对每个已解析的值调用,从最内层值开始、根对象最后。返回转换后的值,返回
undefined会删除该属性。
replacer 的三个用途
1. 过滤与改写字段
最典型的用途是在序列化时剔除或脱敏敏感数据。文档示例:
const data = {
name: 'Alice',
password: 'secret123',
email: 'alice@example.com',
age: 30
}
const safeJSON = JSON.stringify(data, (key, value) => {
if (key === 'password') return undefined // 排除
if (key === 'email') return '***hidden***' // 改写
return value
})
console.log(safeJSON)
// 文档示例输出:'{"name":"Alice","email":"***hidden***","age":30}'
嵌套对象同样会被逐对遍历,所以深层的 password 也会被命中。
2. 用数组做白名单
不想写逻辑时,可以直接传一个字符串数组,输出中只保留列出的属性:
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
password: 'secret',
role: 'admin',
createdAt: '2024-01-01'
}
JSON.stringify(user, ['id', 'name', 'email'])
// '{"id":1,"name":"Alice","email":"alice@example.com"}'
文档提示了一条限制:数组 replacer 只作用于顶层对象属性,不会深入嵌套对象;需要深层过滤时改用 replacer 函数。
3. 利用首次"空 key"调用包裹整个输出
replacer 第一次被调用时,key 是空字符串、value 是整个根对象(文档中称为 initial call)。这让你可以在序列化前替换整个输出结构:
JSON.stringify({ x: 1 }, (key, value) => {
if (key === '') {
return { wrapper: value, timestamp: Date.now() }
}
return value
})
// 文档示例输出:'{"wrapper":{"x":1},"timestamp":1704067200000}'
// 注意 timestamp 来自 Date.now(),实际值随运行时间变化
可选:space 参数控制缩进
第三个参数只影响排版,不影响内容:
const data = { name: 'Alice', address: { city: 'NYC', zip: '10001' } }
JSON.stringify(data, null, 2) // 2 空格缩进
JSON.stringify(data, null, '\t') // Tab 缩进
JSON.stringify({ a: 1, b: 2 }, null, '→ ') // 自定义缩进字符串
文档说明数字会被钳制到 10(传 15 与传 10 结果相同,测试文件中有对应断言),字符串超过 10 个字符会被截断。
reviver 的三个用途
1. 把日期字符串恢复成 Date 对象
JSON 没有 Date 类型,stringify 后日期只剩 ISO 字符串;直接 JSON.parse 拿到的 createdAt 是字符串,调用 getTime() 会抛 TypeError。文档给出的 reviver 用法是按模式识别 ISO 日期串:
const json = '{"name":"Alice","createdAt":"2024-01-15T10:30:00.000Z"}'
const restored = JSON.parse(json, (key, value) => {
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value)
}
return value
})
restored.createdAt instanceof Date // true
restored.createdAt.toISOString() // "2024-01-15T10:30:00.000Z"
上面的两个断言来自仓库测试文件,可作为本段代码的验证标准。若你只关心特定字段,文档还给了按 key 判断的写法:if (key === 'date') return new Date(value)。
2. 解析时删除属性
reviver 返回 undefined 即删除该属性,适合丢弃内部标记字段:
const json = '{"name":"Alice","__internal":true,"id":1}'
const cleaned = JSON.parse(json, (key, value) => {
if (key.startsWith('__')) return undefined
return value
})
console.log(cleaned) // 文档示例输出:{ name: 'Alice', id: 1 }
3. 注意处理顺序:由内向外
reviver 先处理最内层值,最后才轮到根对象:
JSON.parse('{"a":{"b":1},"c":2}', (key, value) => {
console.log(`key: "${key}", value:`, value)
return value
})
文档示例输出(测试文件断言顺序为 ['b', 'a', 'c', '']):
key: "b", value: 1 ← 最内层先处理
key: "a", value: { b: 1 } ← 再处理包含它的对象
key: "c", value: 2
key: "", value: {...} ← 根对象最后
写 reviver 逻辑时要知道:处理父对象时,其子属性已经是 reviver 返回后的值。
特殊类型:Map、Set、BigInt 与循环引用
Map 和 Set 默认序列化成空对象
const map = new Map([['a', 1], ['b', 2]])
JSON.stringify(map) // '{}'
const set = new Set([1, 2, 3])
JSON.stringify(set) // '{}'
文档给出的方案是 replacer/reviver 配对,用 __type 标记还原类型:
function replacer(key, value) {
if (value instanceof Map) {
return { __type: 'Map', entries: Array.from(value.entries()) }
}
if (value instanceof Set) {
return { __type: 'Set', values: Array.from(value) }
}
return value
}
function reviver(key, value) {
if (value && value.__type === 'Map') return new Map(value.entries)
if (value && value.__type === 'Set') return new Set(value.values)
return value
}
const data = {
users: new Map([['alice', { age: 30 }], ['bob', { age: 25 }]]),
tags: new Set(['javascript', 'tutorial'])
}
const json = JSON.stringify(data, replacer)
const restored = JSON.parse(json, reviver)
restored.users instanceof Map // true(测试文件同时断言 users.get('alice') 为 { age: 30 })
restored.tags instanceof Set // true
BigInt 默认直接抛错
const data = { bigNumber: 12345678901234567890n }
JSON.stringify(data) // TypeError: Do not know how to serialize a BigInt
同样用配对处理,把 BigInt 转成带标记的对象再还原:
function bigIntReplacer(key, value) {
if (typeof value === 'bigint') {
return { __type: 'BigInt', value: value.toString() }
}
return value
}
function bigIntReviver(key, value) {
if (value && value.__type === 'BigInt') return BigInt(value.value)
return value
}
const json2 = JSON.stringify({ id: 9007199254740993n }, bigIntReplacer)
// '{"id":{"__type":"BigInt","value":"9007199254740993"}}'
const restored2 = JSON.parse(json2, bigIntReviver)
restored2.id // 9007199254740993n
循环引用:用 WeakSet 在 replacer 中拦截
对象自引用时 stringify 直接抛错:
const obj = { name: 'Alice' }
obj.self = obj
JSON.stringify(obj) // TypeError: Converting circular structure to JSON
文档给出的处理方式是 replacer + WeakSet 记录已见对象(WeakSet 只存对象且不阻止垃圾回收):
function safeStringify(obj) {
const seen = new WeakSet()
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]' // 也可以返回 undefined 直接省略
}
seen.add(value)
}
return value
})
}
const o = { name: 'Alice' }
o.self = o
console.log(safeStringify(o))
// 文档示例输出:'{"name":"Alice","self":"[Circular Reference]"}'
文档还提到 DOM 节点常因父子关系带循环引用,实际做法是只提取需要的字段(如 tagName、id、className)再序列化。
toJSON():对象自己控制序列化
如果对象定义了 toJSON(),stringify 会调用它并用其返回值代替该对象,无需外部 replacer:
const user = {
name: 'Alice',
password: 'secret123',
toJSON() {
return { name: this.name } // password 不会进入输出
}
}
JSON.stringify(user) // '{"name":"Alice"}'
内置 Date 也依赖 toJSON(),所以日期自动变成 ISO 字符串:
JSON.stringify(new Date('2024-01-15T10:30:00Z'))
// '"2024-01-15T10:30:00.000Z"'
在类里定义 toJSON() 时,toJSON 还能收到属性 key 作参数,用来区分自己位于根级还是嵌套:
// 类实例示例(采用测试文件中的固定日期,保证输出可核对)
class User {
constructor(name, email, password) {
this.name = name
this.email = email
this.password = password
this.createdAt = new Date('2024-01-15T10:30:00.000Z')
}
toJSON() {
return {
name: this.name,
email: this.email,
createdAt: this.createdAt.toISOString()
}
}
}
JSON.stringify(new User('Alice', 'alice@example.com', 'secret'))
// '{"name":"Alice","email":"alice@example.com","createdAt":"2024-01-15T10:30:00.000Z"}'
const obj = {
toJSON(key) {
return key ? `Nested under "${key}"` : 'Root level'
}
}
JSON.stringify(obj) // '"Root level"'
JSON.stringify({ data: obj }) // '{"data":"Nested under \\"data\\""}'
JSON.stringify([obj]) // '["Nested under \\"0\\""]'
两者同时存在时的执行顺序(文档 Q&A 与测试文件 Q4 均确认):toJSON() 先运行,replacer 再处理 toJSON() 返回的结果。区别在于 toJSON() 是对象自己控制自身序列化,replacer 是外部对整棵对象树的统一控制。
序列化时哪些值会"丢"或"变形"
写 replacer 前先知道默认行为,很多"字段消失"问题不需要 replacer 也能解释:
const obj = {
name: 'Alice',
greet: function() { return 'Hi!' }, // 函数:被省略
age: undefined, // undefined:被省略
id: Symbol('id'), // Symbol:被省略
count: NaN, // NaN:变成 null
infinity: Infinity, // Infinity:变成 null
nothing: null // null:保留
}
JSON.stringify(obj)
// '{"name":"Alice","count":null,"infinity":null,"nothing":null}'
// 数组里这些值不会被省略,而是变成 null
JSON.stringify([1, undefined, function() {}, Symbol('x'), 2])
// '[1,null,null,null,2]'
另外两点和 reviver 侧直接相关:
- JSON 文本是严格格式:key 不带双引号、单引号、尾逗号、注释都会让
JSON.parse抛SyntaxError(如JSON.parse('{name: "Alice"}'))。 - 用
JSON.parse(JSON.stringify(obj))做深拷贝只适用于 JSON 安全对象:函数、undefined、Symbol 和原型链都会丢失,文档建议复杂对象改用structuredClone()。
如何验证这些行为
本文所有代码块都来自文档正文,注释里的预期输出可直接用于比对:把代码粘到 Node 或浏览器控制台执行,输出与注释一致即行为正确;含 Date.now() 的输出(包裹示例中的 timestamp)会随运行时间变化,属于文档示例值。
如果想在仓库层面核对,这些行为已被固化为 vitest 断言,集中在 json-deep-dive.test.js(例如 replacer 过滤、reviver 顺序、循环引用输出字符串、BigInt 还原等都有对应 it 用例)。package.json 中的 npm test 脚本执行 vitest run,配合 vitest.config.js 的 include: ['tests/**/*.test.js'](node 环境)会跑遍全部概念的测试,不只是本文主题,所以把它当作整仓回归,而不是单点验证命令;核对本文主题时直接阅读上述测试文件中的断言即可。
下一步
- 想把这类 JSON 存读落到浏览器存储里,可参考项目内的 localStorage & sessionStorage 文档,其中给出了带
try/catch的 JSON 存取封装。 - 解析第三方 JSON 文本时,
JSON.parse的SyntaxError该如何处理,见 Error Handling 文档。 - 本文的 replacer/reviver 机制依赖对象属性访问语义,前置概念可查阅 Object Methods 文档。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00