先记住这个答案
Date 经过 JSON.stringify 会调用 toISOString() 变成字符串,JSON 本身没有日期类型,所以 JSON.parse 回来还是字符串。JSON.parse(text, reviver) 的第二个参数是一个回调,解析完成后会对每个属性调用一次,参数是 key 和 value,返回值会替换原来的值。恢复日期的做法是:只在约定的日期字段上把字符串转成 Date,其他字段原样返回。reviver 的调用顺序是从里到外,先处理最深的属性,最后以空字符串为 key 处理根对象;返回 undefined 会删掉这个属性。
- JSON 里的日期只是字符串,不会自动变回 Date
- reviver 从最深的属性开始,最后处理根
- 返回
undefined会删掉这个属性 - 按字段名转换,别见到像日期的字符串就转
按字段恢复日期,并检查日期是否真实存在
接口返回的对象里有创建时间、过期时间,还有一个长得像日期的普通标签。只转该转的。
const text =
'{"createdAt":"2026-09-10T08:30:00.000Z","label":"2026-09-10","expireAt":"2026-02-30T00:00:00.000Z"}'
const DATE_FIELDS = new Set(['createdAt', 'expireAt'])
function toDate(value) {
const date = new Date(value)
// 读回来和原文对不上,说明日期被引擎挪过了,比如 2 月 30 日
return !Number.isNaN(date.getTime()) && date.toISOString() === value ? date : null
}
const data = JSON.parse(text, (key, value) =>
DATE_FIELDS.has(key) && typeof value === 'string' ? toDate(value) : value
)
console.log(data.createdAt instanceof Date, typeof data.label, data.expireAt)查看输出与解释
true string nullcreatedAt 变成了 Date。label 虽然长得像日期,但不在字段列表里,保持字符串。
expireAt 是 2 月 30 日,一个不存在的日期。在 Chrome 和 Node 里,new Date('2026-02-30T00:00:00.000Z') 不会得到 Invalid Date,而是被挪到 3 月 2 日,只检查 Number.isNaN(date.getTime()) 拦不住。所以 toDate 多做了一步:把 Date 转回 ISO 字符串,和原文对比,对不上就说明被挪过了。这个校验要求接口统一用 toISOString() 的标准格式,实际项目里通常和后端约定好。
- 为什么不用正则匹配所有日期字符串
- 有人会写
/^\d{4}-\d{2}-\d{2}/.test(value)就转 Date,这会把版本号、订单编号、只有日期的生日字段都误转。生日「1990-05-20」转成 Date 后是 UTC 零点,在美洲时区显示成 5 月 19 日。什么字段是时间点,应该由接口约定决定,而不是看字符串长什么样。
调用顺序和返回值规则
reviver 的两条规则最容易踩坑:从里到外调用,返回 undefined 删属性。
const order = []
JSON.parse('{"a":{"b":1,"c":[2]},"d":3}', (key, value) => {
order.push(key === '' ? '(root)' : key)
return value
})
console.log(order.join(' -> '))
const safe = JSON.parse('{"a":1,"password":"x"}', (key, value) =>
key === 'password' ? undefined : value // 返回 undefined,属性被删
)
console.log(JSON.stringify(safe))
console.log(JSON.parse('{"a":1}', (key, value) => { if (key === 'a') return value * 2 }))查看输出与解释
b -> 0 -> c -> a -> d -> (root)
{"a":1}
undefined第一行是调用顺序。b 和 c 比 a 先处理,数组元素的 key 是字符串 '0',根对象最后处理,key 是空字符串。所以父级回调拿到的是子属性已经处理完的结果,可以在父级把恢复好的字段组装成领域对象。
第三行是最常见的 bug。回调只在 key === 'a' 时有返回值,其他情况函数默认返回 undefined。轮到根对象时也返回了 undefined,整个解析结果就没了。写 reviver 的第一件事,就是在最后加上 return value。
容易答错的地方
- 以为
JSON.parse能自动识别日期 - JSON 只有字符串、数字、布尔、
null、对象和数组六种类型。日期序列化后就是字符串,解析时不会有任何特殊处理。 - 忘记默认
return value - 箭头函数只写了条件分支,其他属性全部返回
undefined,被逐个删掉,最后连根对象也没了。用三元表达式或者在函数末尾统一return value能避免。
面试官还会怎么问?
reviver 里的 this 指向什么?
用普通函数写时,this 是当前属性所在的对象,可以用它读取同级的其他字段,比如根据 this.type 决定怎么转换。箭头函数没有自己的 this,拿不到这个对象。
字段很多、结构很深时,还用 reviver 吗?
reviver 适合「所有叫 createdAt 的字段都转 Date」这种简单统一的规则。复杂接口更适合先 JSON.parse,再用 zod 这类 schema 库校验并转换,出错时能明确告诉你是哪个字段、什么原因,而不是悄悄变成 null。
只有日期没有时间的字段,比如生日,要转成 Date 吗?
一般不转。Date 表示的是一个时间点,生日是一个日历日期,转成 Date 就要选一个时区的零点,换个时区显示就可能差一天。保持 YYYY-MM-DD 字符串,只在显示时格式化,是更稳的做法。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。