JSON.stringify 把字段悄悄省略:undefined 不能代替清空指令
- 发布时间:2026-10-23 22:45:04
- 本文热度:浏览 2 赞 0 评论 0
- 文章标签: JavaScript JSON 接口设计
- 全文共1字,阅读约需1分钟
更新个人资料时,前端明明放了 bio: undefined,请求里却没有bio。后端按“缺失表示保持原值”处理,用户以为已经清空,重新打开页面旧内容还在。
JSON.stringify对undefined不是统一转成null。对象中的相应属性通常被省略,数组中的对应位置则变成null;根值本身是undefined时,返回值甚至不是一段JSON字符串。用同一个“空值”概念理解这三条路径,会把字段更新意图丢在序列化边界。
先打印真正准备发出去的文本
把下面程序保存为patch-contract.js,使用现代浏览器控制台或Node.js运行。它先展示序列化规则,再给一个范围很小的补丁接口定义明确合同:字段缺失表示不修改,null表示清空,字符串表示设为新内容,undefined直接拒绝。
'use strict';
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const objectText = JSON.stringify({ name: 'Lin', bio: undefined, empty: null });
const arrayText = JSON.stringify(['Lin', undefined, , null]);
console.log('object:', objectText);
console.log('array:', arrayText);
console.log('root undefined:', JSON.stringify(undefined));
assert(objectText === '{"name":"Lin","empty":null}', 'object omission');
assert(arrayText === '["Lin",null,null,null]', 'array placeholders');
assert(JSON.stringify(undefined) === undefined, 'root result is not a string');
function encodeProfilePatch(input) {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('profile patch must be an object');
}
const allowed = new Set(['displayName', 'bio']);
for (const key of Object.keys(input)) {
if (!allowed.has(key)) throw new TypeError(`unknown field: ${key}`);
}
const patch = {};
for (const key of allowed) {
if (!Object.hasOwn(input, key)) continue;
const value = input[key];
if (value !== null && typeof value !== 'string') {
throw new TypeError(`${key}: string or null required`);
}
patch[key] = value;
}
return JSON.stringify(patch);
}
console.log('leave unchanged:', encodeProfilePatch({}));
console.log('clear bio:', encodeProfilePatch({ bio: null }));
console.log('set empty text:', encodeProfilePatch({ bio: '' }));
try {
encodeProfilePatch({ bio: undefined });
throw new Error('undefined should not silently disappear');
} catch (error) {
if (!(error instanceof TypeError)) throw error;
console.log('undefined patch: rejected');
}
const log = [];
const source = {
toJSON(key) { log.push(`toJSON:${key}`); return { safe: 3 }; }
};
const transformed = JSON.stringify({ item: source }, (key, value) => {
if (key === 'item') log.push(`replacer sees safe:${value.safe}`);
return value;
});
console.log('transformed:', transformed);
console.log('order:', log.join(' | '));
assert(transformed === '{"item":{"safe":3}}', 'toJSON transformation');
assert(log[0] === 'toJSON:item' && log[1] === 'replacer sees safe:3', 'execution order');
对象里bio消失,数组里undefined和空槽都占一个null位置,根undefined仍然是JavaScript的undefined。三个结果都没有抛异常,不代表它们承载同一种业务意图。
补丁编码器则把清空、空字符串与不修改分别保留下来。这里null的含义是本接口规定的清空,不是JSON本身规定所有后端都应该如此处理。数据库是否允许NULL、清空后是否改成空字符串,也应由服务端合同决定。
用 hasOwn 区分没有字段与字段值为空
如果直接写 if (input.bio),空字符串和null都会被当成假,无法表达清空。若只检查 input.bio !== undefined,缺失与显式undefined又被合成同一种状态。需要区分字段存在性时,应先确认自有属性,再验证允许值。
示例不接受任意对象图,只处理两项标量字段。实际接口还应限定文本长度、字段格式与授权范围,不能仅靠JSON.stringify成功就把数据视为合规。字符串里包含多少字符、是否允许空白,仍然是输入验证的一部分。
输入对象若来源于应用内部模型,还可能带getter或自定义原型。编码器读取字段时可以执行这些行为,因此更稳妥的调用方式是传入明确的数据传输对象,或在接收JSON后验证其模型。它不是任意对象的安全净化器。
用 replacer 全局把 undefined 变 null,可能改错合同
写一个replacer把undefined返回为null,确实可以让对象字段不再消失。但它同时把所有嵌套位置都解释成显式空值;原本表示“不知道”“没有提供”的状态可能被改成清空请求。
在资料补丁里,全局替换尤其危险。某个表单控件没挂载就产生undefined,本应保持旧值,通用替换却可能清掉后端数据。正确做法是先把表单意图转换成接口允许的状态,再序列化,而不是让序列化回调猜用户想做什么。
数组也不是对象规则的简单延伸。位置常有含义,省掉某个元素会让后续下标移动,因此JSON表示选择保留null占位。但应用若要求数组每项必须是合法对象,应该在序列化前拒绝空槽或undefined,不能让null替你掩盖缺项。
toJSON 可以在 replacer 之前改变数据
最后一组输出显示,source先用toJSON变成safe对象,replacer在item位置看到的已经是转换结果。replacer不是保证能见到所有原始字段的一道审计关卡。
一些内置对象也有自己的JSON表示。序列化之后的结构未必还能恢复原来的类型,更不能指望JSON往返深拷贝保留方法、资源或对象身份。本文只围绕字段意图,不把它包装成通用克隆办法。
如果日志与实际请求不一致,应同时查看序列化前的数据模型和最终文本。浏览器控制台展示对象有时反映后续状态,而请求发送的是某一时刻生成的字节。把最终JSON字符串作为受控调试证据,比展开一个稍后才观察的对象更明确;其中敏感字段仍需按日志规则处理。
数值字段还有另一种静默变形:非有限数值可以在JSON表示里变成null。若把计算失败产生的NaN直接放进请求,后端看见的可能是一个合法空值,而不是前端算术错误。需要有限数值的字段应在编码前使用明确检查,不能等到JSON文本生成以后再猜null来自哪里。
这也说明序列化前的模型验证不是重复劳动。字段存在性、值域和更新意图在JavaScript里更丰富,进入JSON之后某些区别已经不可逆地消失。后端仍然要验证JSON本身,但它无法知道一个缺失字段最初是用户没改、表单没加载,还是被undefined静默省略。
循环引用与BigInt等还有各自失败路径,不能因为本例undefined静默处理,就认为所有不支持的值都会自动消失。调用方应让序列化异常进入明确失败分支,避免UI显示保存成功却没有形成请求。
toJSON和replacer本身也可能抛异常。它们不是只影响显示的格式选项,而是编码过程中实际执行的代码;错误处理应围绕完整编码操作建立,并且只有文本成功形成后才进入请求发送阶段。编码失败时也要保留尚未提交的用户输入。
重新验证清空资料时,至少核对三份证据:编码器返回含bio:null的文本,服务端按清空合同应用更新,重新读取结果符合预期。第一步若已经丢掉字段,继续排查数据库默认值通常只会把真正问题绕过去。