侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 148 篇文章
  • 累计收到 7 条评论

reviver 从最里层往外跑,父节点拿到的是改过的值

2026-9-29 / 0 评论 / 6 阅读

reviver 从最里层往外跑,父节点拿到的是改过的值

接口返回一段嵌套 JSON,解析的那一刻顺手把字符串两头的空格去掉,顺便校验一下业务码。很自然的一种写法:

const data = JSON.parse(text, function (key, value) {
  if (key === 'code' && value !== '200') throw new Error('业务码不对');
  return typeof value === 'string' ? value.trim() : value;
});

这段代码有两处会出意外,都跟 reviver 的调用时机有关。下面每个数字都是在本机跑出来的,输入统一用这一小段:

const text = '{"id":7,"user":{"name":"x","age":30},"tags":["a","b"]}';

先子后父,根那一层最后到

reviver 里只打印 key 和 value,八个节点的到达次序是:

== 1. reviver 被调用的顺序 ==
   1. key=id     value=7
   2. key=name   value="x"
   3. key=age    value=30
   4. key=user   value={对象}
   5. key=0      value="a"
   6. key=1      value="b"
   7. key=tags   value=[a b]
   8. key=<根>    value={对象}

id、name、age 先在叶子层跑完,然后才是包着它们的 user;数组的 0、1 跑完,才是 tags;最后一个是根,key 是空串。规范里的做法是递归下降:处理一个对象时,先对它的每个自有键递进去拿到结果,再调用 reviver。所以 reviver 收到 value 的时候,它下面那棵子树已经全部处理完了。

想校验整棵树,就只能在根那一次做,那时每个节点都已经被 reviver 的返回值替换过。

key 是字符串,数字键也是

值为 {"0":"zero","a":"A"} 的对象,三次调用拿到的 key:

== 3. key 是字符串,数字键也不例外 ==
  key = "0"  typeof key = string
  key = "a"  typeof key = string
  key = ""  typeof key = string

三次都是字符串。解析出来的对象键是 "0",不是数字 0,所以判断用 key === '0' 还是 key === 0 在 JSON 场景里没有第二种选择。根那一次的 key 是空串,不少框架就是靠 key === '' 判断「最外层到了」。

这一层的 this 是持有它的对象

同一个案例里,reviver 内部的 this 指向正在被处理的那个对象,不是全局对象:

== 5. 这一层的 this 指向哪个对象 ==
  key=id     this 的键 = [id user tags]
  key=name   this 的键 = [name age]
  key=age    this 的键 = [name age]
  key=user   this 的键 = [id user tags]
  key=0      this 的键 = [0 1]
  key=1      this 的键 = [0 1]
  key=tags   this 的键 = [id user tags]
  key=<根>    this 的键 = []

叶子层的 this 是 {name, age},tags 那两个元素的 this 是数组本身。用普通函数写法时这个上下文可用,换成箭头函数就没有了,得靠闭包传参。

返回 undefined 在三个位置有三种结果

这是最容易写错的一处。返回 undefined 不等于「保留一个 undefined 值」:

== 4. 返回 undefined 会怎样 ==
  对象属性:{"id":7,"user":{"name":"x"},"tags":["a","b"]}
  数组元素:JSON.stringify 得到 [1,null,3],length = 3,下标 1 还在吗 = false
  数组元素:下标 1 的值 = undefined
  根上返回 undefined:undefined

对象属性上返回 undefined,属性被删掉,结果里没有 age 这个键。数组元素上返回 undefined,元素变成空槽,length 还是 3,1 in arr 是 false,JSON.stringify 之后那一格变成 null。根上返回 undefined,整个 JSON.parse 的返回值就是 undefined。

用 reviver 过滤字段的人常在这条上翻车:想让数组少一个元素,写 return undefined,拿到的是一个 null 占位,下游按数组长度算偏移的地方就跟着错位。

调用次数跟节点数挂钩

对小对象来说 reviver 的开销可以忽略,数据规模上去就不一样了:

== 6. 一个 key 调用几次 ==
  4 个节点的输入,reviver 调用 4 次
  1000 个元素的数组,reviver 调用 1001 次

4 个节点的输入调用 4 次,1000 个元素的数组调用 1001 次,多出来的那次是根。这个次数是按节点数走的,跟 JSON 文本多长没关系,所以一个几十 KB 的浅数组和一棵同样大小的深树,调用次数差别很大。

顺序带来的后果

把 trim 和整体校验写进同一个 reviver,根那一次看到的就是已经改过的数据:

== 7. 顺序带来的一个实际后果:到根上校验时,子节点已经被改过 ==
  原始文本 {"code":" 200 ","msg":" ok "}
  根这一层拿到的是 {"code":"200","msg":"ok"},校验和就地修改只能挑一个
  不在 reviver 里改,交给外面处理:{"code":" 200 ","msg":" ok "}
  外面再改一次的结果:{"code":"200","msg":"ok"}

原始文本里 code 是 " 200 ",带空格;到根这一层,它已经变成 "200"。如果校验的规则是「字段不该有多余空白,发现就报错」,这条规则永远不会触发,因为违规的值在到达校验点之前就被修掉了。

常见的坑就在这里:同一个 reviver 里既做清洗又做校验,清洗总是先赢。

适合和不适合

reviver 的强项是类型还原和结构改写。把字符串还原成日期,把下划线命名的键改成驼峰,或者拆掉一层 { data: ... } 包装,这些都是一遍递归就能做完的事,比自己写递归函数稳。

不适合的是三件事:大范围的格式校验,此时的值已经不是原文里那个;按数组下标做增删,返回值语义跟想象的不一样;把大数组原样过一遍 reviver,调用次数按节点数走。

我的取舍是:reviver 只做类型还原,一行判断,不写业务规则。校验另起一步,拿到的是最终对象,读起来也清楚。这一条没有性能上的理由,纯粹是前面那个「清洗先赢」的坑踩过之后改的。

    🤞 分享