学完这篇,你能把任意 HTML 表单在「FormData」和「JSON」之间自由转换,并且知道哪些字段会在转换中丢失、该怎么补救。
第一步:先把表单变成 FormData
最省事的做法是把 <form> 元素直接传给 FormData 构造函数,浏览器会自己收集所有带 name 的控件:
<form id="f">
<input name="username" value="阿乐">
<input type="password" name="password" value="123456">
<select name="gender"><option value="1" selected>男</option></select>
<input type="checkbox" name="tags" value="php" checked>
<input type="checkbox" name="tags" value="js" checked>
<input type="file" name="avatar">
</form>
const fd = new FormData(document.getElementById('f'));
不依赖表单时,用 append() 手工拼:
const fd = new FormData();
fd.append('username', '阿乐');
fd.append('tags', 'php');
fd.append('tags', 'js');
注意:没有 name 属性的控件不会进 FormData;未勾选的 checkbox / radio 也不会,这是「丢失字段」最常见的来源,别以为是代码写错了。
第二步:FormData 转 JSON
FormData 不能直接 JSON.stringify,它序列化出来是 {}。必须遍历它的 entries():
function formDataToObject(fd) {
const obj = {};
for (const [key, value] of fd.entries()) {
if (key in obj) {
if (!Array.isArray(obj[key])) obj[key] = [obj[key]];
obj[key].push(value);
} else {
obj[key] = value;
}
}
return obj;
}
const data = formDataToObject(fd);
console.log(JSON.stringify(data));
这里对重复的 name(比如多个 checkbox、多选 select)做了合并成数组的处理。
很多人图快写成 Object.fromEntries(fd),一行搞定,但同名键只保留最后一个值,tags 就只剩 "js" 了。
注意:转换出来的值全是字符串。gender 是 "1" 不是 1,数字型字段(价格、ID、数量)如果后端做了严格类型校验,要么前端 Number(v) 转一遍,要么后端做类型转换。
注意:如果表单里有 <input type="file"> 且已选文件,值是 File 对象,JSON.stringify 会把它变成 {},文件就静默丢了。二进制必须走 FormData 或先转 base64 / 先单独上传拿 URL。
发请求时:
fetch('/api/user/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
第三步:JSON 转 FormData
反过来常用于「把接口拿到的对象填进表单再提交」,或者调用只收表单格式的老接口:
function objectToFormData(obj) {
const fd = new FormData();
for (const [key, value] of Object.entries(obj)) {
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
value.forEach(v => fd.append(key, v instanceof File ? v : String(v)));
} else {
fd.append(key, value instanceof File ? value : String(value));
}
}
return fd;
}
关键点有三个:
undefined / null 直接跳过,否则会变成字符串 "undefined" 提交上去。
- 非文件值统一
String(),因为 append 会隐式转字符串,显式写出来是为了让你知道类型已经变了。
File / Blob 要原样放进去,不能被 String() 掉。
发送时千万别手动设置 Content-Type:
fetch('/api/upload', { method: 'POST', body: objectToFormData(data) });
注意:FormData 的 Content-Type 是 multipart/form-data; boundary=----xxxx,boundary 由浏览器生成。你一旦手写 headers: {'Content-Type': 'multipart/form-data'},boundary 就没了,后端解析直接失败。
第四步:需要 URL 编码格式时用 URLSearchParams
有些接口收的是 application/x-www-form-urlencoded,这时候用 URLSearchParams 更合适:
const usp = new URLSearchParams(formDataToObject(fd));
// 或者直接 new URLSearchParams(new FormData(form))
fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: usp
});
注意:URLSearchParams 也不支持文件,遇到 File 会变成 "[object File]"。
第五步:把 JSON 回填到表单
收集和回填是配套的,回填时注意 checkbox 和数组:
function fillForm(form, data) {
for (const [key, value] of Object.entries(data)) {
const els = form.querySelectorAll(`[name="${key}"]`);
els.forEach(el => {
if (el.type === 'checkbox' || el.type === 'radio') {
el.checked = Array.isArray(value) ? value.map(String).includes(el.value) : String(value) === el.value;
} else {
el.value = Array.isArray(value) ? value.join(',') : value ?? '';
}
});
}
}
小结
new FormData(form) 一行收集表单,但只收有 name 且处于选中/勾选状态的控件。
- FormData 转 JSON 要遍历
fd.entries();Object.fromEntries 会吞掉同名字段,多选场景别用。
- 转换后所有值都是字符串,数字和布尔要自己转类型。
File 无法进 JSON,文件上传只能走 FormData 或先单独上传。
- JSON 转 FormData 时跳过
null / undefined,其余值显式 String(),文件原样 append。
- 发 FormData 绝对不要手写 Content-Type,boundary 交给浏览器。