首页
/ JSON.stringify 与 JSON.parse 的 replacer、reviver 能做什么

JSON.stringify 与 JSON.parse 的 replacer、reviver 能做什么

2026-09-08 18:16:25作者:幸俭卉

JSON.stringify()JSON.parse() 看起来只有"转字符串"和"还原对象"两个动作,但它们的第二、第三个参数(replacerspacereviver)决定了序列化过程中每个字段的去留、改写和还原方式。常见的真实需求是:发给 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 节点常因父子关系带循环引用,实际做法是只提取需要的字段(如 tagNameidclassName)再序列化。

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.parseSyntaxError(如 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.jsinclude: ['tests/**/*.test.js'](node 环境)会跑遍全部概念的测试,不只是本文主题,所以把它当作整仓回归,而不是单点验证命令;核对本文主题时直接阅读上述测试文件中的断言即可。

下一步

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391