JavaScript JSON.stringify:同一个 undefined,在对象里消失,在数组里变成 null
一个设置页面把备注改成 undefined,希望服务器清空它。调试时对象里确实有这个字段,但发出的 JSON 却没有,服务器因而保留旧值。另一个页面把同样的值放进数组,收到的却是 null。问题不在网络传输,而在编码前没有定义清楚“没有值”的含义。
JSON 没有 undefined 这个数据类型。默认序列化对象时,对应字段被省略;序列化数组元素时,这个位置以 null 表示;直接序列化顶层 undefined,返回的是 JavaScript 的 undefined,而不是内容为 undefined 的字符串。这三个结果必须分别检查。
先把位置差异变成断言
把示例保存为 demo.mjs,运行 node demo.mjs。它不发送请求,只打印即将传输的文本,再解析回来核对键是否存在。第一段故意让对象、数组和顶层拿到完全相同的输入值,避免把页面表单的其他转换掺进判断。
AI概念示意图:以抽象物件说明本文主题,不代表真实界面或运行结果。
import assert from 'node:assert/strict';
const objectText = JSON.stringify({theme: 'dark', note: undefined});
const arrayText = JSON.stringify([undefined, 3]);
const top = JSON.stringify(undefined);
assert.equal(objectText, '{"theme":"dark"}');
assert.equal(arrayText, '[null,3]');
assert.equal(top, undefined);
assert.equal(Object.hasOwn(JSON.parse(objectText), 'note'), false);
console.log(objectText);
console.log(arrayText);
console.log(`top-is-undefined=${top === undefined}`);
const replaced = JSON.stringify({note: undefined},
(key, value) => value === undefined ? null : value);
assert.equal(replaced, '{"note":null}');
console.log(replaced);
const patch = {set: {theme: 'dark'}, clear: ['note']};
const wire = JSON.stringify(patch);
const received = JSON.parse(wire);
assert.deepEqual(received, {set: {theme: 'dark'}, clear: ['note']});
console.log(wire);输出存在,不代表语义保持不变
前两行分别是只剩 theme 的对象和含 null、三的数组。第三行 top-is-undefined=true 表明顶层结果根本不是字符串。发送之前应验证编码结果类型;若直接交给另一个接收字符串的函数,它可能自行转换成文字,进一步掩盖第一次变化发生的位置。
对象解析回来的 note 已经不存在,因此读取它虽然也得到 undefined,却无法证明字段保留了。示例用 Object.hasOwn 检查存在性,把“缺少字段”和“字段存在但值不可用”分开。数组则保留原位置,不能根据得到 null 就推断用户明确要求清空。
为什么通用替换器仍然不够
把所有 undefined 替换成 null 的 replacer 能让字段出现,第四行展示了这种结果。但这是主动改变数据约定:它让未填写、读取失败和明确清空都可能变成同一种表示。只有接口双方已经约定 null 的意义,才应采用这类规则。
替换器也无法知道一个对象里从未存在的字段是不是用户忘记填写。它处理的是当前遍历到的数据,而不是用户操作历史。若需要校验必填项,应在序列化之前按表单规则检查;不能指望回调从缺少的键中自动恢复用户意图。
把修改和清空写进传输格式
最后一段采用这个示例自行约定的格式:set 放要修改的值,clear 放要清空的字段名。编码后再解析,结果依然区分主题修改与备注清空。这不是所有服务器都支持的通用协议,必须同时实现接收端校验,限制字段名,并定义同一字段出现于两处时的处理。
如果既有接口把缺失视为保留、null 视为清空,就按该契约构造普通对象,并为三种用户动作各写一例。测试应比较解析后的实际请求体,而不是比较编码前的表单对象。只有传出去的结构仍表达正确意图,保存成功才有业务意义。
接收端还应拒绝不支持的操作名,并检查 clear 中的元素是不是字符串。若批量表单按数组位置对应条目,最好同时携带明确的记录标识,不能把占位值误作删除指令。展示层的空白也应由页面自己决定,不要反过来改变传输状态。
本例关注 undefined 的位置差异,不把 JSON 往返当作通用克隆。函数、Symbol、非有限数字、自定义 toJSON 以及循环引用各有额外规则。传输对象越接近经过校验的纯数据,越容易逐项解释和验收;复杂运行时对象应先转成清楚的数据结构。


