JavaScript JSON.parse reviver:父对象收到的值,为什么已经被孩子改过
读取配置时,希望把计数转换成需要的形式,再去掉只供调试的字段。有人把这些逻辑放进 JSON.parse 的第二个参数,却发现父对象已经变了,甚至整份结果变成 undefined。问题通常来自对 reviver 调用时机和返回值的误解。
reviver 接到的不是尚未解析的源码片段,而是解析后的一项值。嵌套数据会先处理内部成员,再把处理过的父对象交给回调。返回什么,就决定这一项留下什么;漏写 return 也会返回 undefined,因而具有实际删除效果。
记下回调顺序,再检查每一层的结果
把完整代码存为 demo.mjs,执行 node demo.mjs。本文在 Node.js v24.19.0 运行成功。样本全部写在源码里,先验证普通对象,再检查数组、根结果以及语法错误,不需要额外依赖。
AI概念插图:用抽象图形表现本文讨论的关系,并非软件截图或真实运行结果。
import assert from 'node:assert/strict';
const order = [];
const result = JSON.parse(
'{"box":{"count":2,"debug":true},"keep":1}',
function (key, value) {
order.push(key === '' ? '<root>' : key);
if (key === 'debug') return undefined;
if (key === 'count') return value * 10;
if (key === 'box') assert.deepEqual(value, { count: 20 });
return value;
}
);
assert.deepEqual(order, ['count', 'debug', 'box', 'keep', '<root>']);
assert.deepEqual(result, { box: { count: 20 }, keep: 1 });
console.log(order.join(' -> '));
console.log(JSON.stringify(result));
const array = JSON.parse('[10,20,30]', (key, value) =>
key === '1' ? undefined : value);
assert.equal(array.length, 3);
assert.equal(Object.hasOwn(array, '1'), false);
console.log('array length', array.length, 'index1 exists', Object.hasOwn(array, '1'));
const removedRoot = JSON.parse('{"x":1}', (key, value) =>
key === '' ? undefined : value);
assert.equal(removedRoot, undefined);
console.log('root removed', removedRoot === undefined);
let calls = 0;
assert.throws(() => JSON.parse('{"x":}', (key, value) => {
calls++;
return value;
}), SyntaxError);
assert.equal(calls, 0);
console.log('invalid JSON reviver calls', calls);父对象收到的是已经处理过的孩子
第一行打印 count、debug、box、keep,最后是根回调。count 从二变成二十,debug 被删除,因此进入 box 的分支时,父对象已经只剩 count。断言就在该分支内部运行,直接检查转换发生的阶段,而不是只比较最终字符串。
回调中的 return value 是保留其他字段的必要分支。如果只处理 count,却忘记让其余情况返回原值,那么 box、keep 乃至最终根都会受到影响。排错时,应先检查有没有一条路径默默返回 undefined,再怀疑 JSON.parse 本身。
这里按字段名称实施规则,是为了让实验清楚。实际数据中,不同层级可能拥有同名字段。只看 key 等于 count,会处理所有同名成员;需要限定路径时,应先设计结构约束或解析后的显式遍历,不要默认字段名在整棵树里唯一。
删除数组成员与缩短数组不同
数组的键以字符串索引传给回调。第二段删除索引一之后,长度仍是三,但该位置不再有自有属性。它不是把后面的三十自动向左挪动。若业务需要连续紧凑的结果,应在数组这一层有意识地重建列表,而不是把返回 undefined 当成过滤操作。
根值也会再被调用一次,键是空字符串。第三段在这次调用返回 undefined,整份解析结果就变成 undefined。需要包装或替换根对象时可以使用这一步;需要保留根时,则必须让它得到合适返回值。
空字符串也能成为 JSON 对象的真实字段名,因此仅凭 key 为空,不能在任意输入中断定当前一定是根。本例第三段的输入明确没有空键,才用这个简化条件展示效果。通用转换器要把这种可能性纳入设计。
最后一段输入缺少字段值,产生 SyntaxError,回调次数为零。reviver 不会在语法解析之前拦截并修补损坏文本,也不是流式解析器。它适合对合法 JSON 的值做转换,输入语法、数据形状和业务约束仍需分别验证。
验收转换规则时,至少保留一个不该变化的兄弟字段,并观察父对象、数组长度和最终根。这样能同时发现漏返回、误删和层级理解错误,避免“某个目标字段变对了”掩盖其他部分已经丢失。


