详细讲解在浏览器中使用JavaScript将HTML表单数据序列化为JSON格式的方法,涵盖FormData基础用法、多值字段处理、form.elements类型转换及常见注意事项。
JavaScript将表单数据序列化为JSON格式的方法
时间:2026-11-13
浏览:116
文章目录
在浏览器中用 JavaScript 将 HTML 表单数据序列化为 JSON 字符串,最直接的做法是先用 new FormData(form) 收集表单控件的键值对,再把键值对转为普通 JavaScript 对象,最后调用 JSON.stringify() 生成 JSON 字符串。当表单包含多选框、同名多值字段,或需要保留数字与布尔类型时,则需要在此基础上按规则聚合数组或直接遍历 form.elements。
一、基础方法:FormData 配合 Object.fromEntries
对于大多数只包含单值字段(如文本输入框、密码框、单选按钮、单选下拉框)的表单,浏览器内置的 FormData 和 Object.fromEntries() 就能完成转换,不需要手动逐个读取 DOM 元素的 value。
实际操作时需要注意三个前提条件,它们直接决定了字段能否进入最终的 JSON:
- 控件必须设置
name属性,FormData会把name作为键名,只有id没有name的控件会被忽略。 - 处于
disabled状态的控件不会被FormData收集;若需要不可编辑但仍参与提交,应使用readonly。 - 未勾选的复选框和未选中的单选框不会出现在
FormData中。
下面是一个可以直接在浏览器 HTML 页面中运行的完整示例。假设页面上有一个用户注册表单,在提交事件中阻止默认整页跳转,然后把表单序列化为 JSON 字符串:
<form id="userForm">
<label>用户名:<input type="text" name="username" value="zhangsan" /></label>
<label>邮箱:<input type="email" name="email" value="zhangsan@example.com" /></label>
<label>部门:
<select name="department">
<option value="dev" selected>研发部</option>
<option value="ops">运维部</option>
</select>
</label>
<button type="submit">提交</button>
</form>
<script>
const userForm = document.getElementById('userForm');
userForm.addEventListener('submit', function (event) {
event.preventDefault();
const formData = new FormData(userForm);
const payload = Object.fromEntries(formData.entries());
const jsonString = JSON.stringify(payload);
console.log(jsonString);
});
</script>
点击提交按钮后,在浏览器开发者工具的控制台中会看到如下结果(示例输出):
// 示例输出
{"username":"zhangsan","email":"zhangsan@example.com","department":"dev"}
FormData 实例本身实现了可迭代协议,formData.entries() 会逐条产出 [key, value] 二元组,因此 Object.fromEntries(formData) 与 Object.fromEntries(formData.entries()) 效果相同。为了代码意图清晰,保留 .entries() 更便于阅读。
二、处理多值字段:复选框组与多选下拉框
Object.fromEntries() 有一个很容易忽略的限制:当表单里存在多个相同 name 的值时,例如多选复选框(<input type="checkbox" name="hobby">)或开启了 multiple 属性的 <select>,后出现的同名键会直接覆盖前面出现的值,最终只剩下最后勾选的那一项。
如果表单存在多值字段,就不要直接调用 Object.fromEntries(),而应遍历 FormData,在键名第一次出现时存成单值,再次出现时合并为数组;或者对已知的多值字段显式使用 formData.getAll(key) 保证它始终输出数组(即使只勾选了一项)。
下面写一个通用的辅助函数 serializeFormToJson,先对重复键自动聚合成数组,同时允许通过第二个参数传入“始终按数组处理”的字段名列表(防止用户只勾选一个复选框时字段从数组变成字符串):
<form id="profileForm">
<input type="text" name="nickname" value="林青" />
<label><input type="checkbox" name="hobby" value="reading" checked /> 阅读</label>
<label><input type="checkbox" name="hobby" value="coding" checked /> 编程</label>
<label><input type="checkbox" name="hobby" value="music" /> 音乐</label>
<select name="skills" multiple>
<option value="js" selected>JavaScript</option>
<option value="css">CSS</option>
</select>
<button type="submit">保存</button>
</form>
<script>
function serializeFormToJson(form, arrayFields = []) {
const formData = new FormData(form);
const payload = {};
const arrayFieldSet = new Set(arrayFields);
for (const [key, value] of formData.entries()) {
if (arrayFieldSet.has(key)) {
if (!Array.isArray(payload[key])) {
payload[key] = [];
}
payload[key].push(value);
continue;
}
if (Object.prototype.hasOwnProperty.call(payload, key)) {
if (!Array.isArray(payload[key])) {
payload[key] = [payload[key]];
}
payload[key].push(value);
} else {
payload[key] = value;
}
}
for (const key of arrayFieldSet) {
if (!Object.prototype.hasOwnProperty.call(payload, key)) {
payload[key] = [];
}
}
return JSON.stringify(payload);
}
const profileForm = document.getElementById('profileForm');
profileForm.addEventListener('submit', function (event) {
event.preventDefault();
const jsonString = serializeFormToJson(profileForm, ['hobby', 'skills']);
console.log(jsonString);
});
</script>
上述代码提交后的结果如下(示例输出):
// 示例输出
{"nickname":"林青","hobby":["reading","coding"],"skills":["js"]}
即使 skills 只选中了一项,或者 hobby 一项都没选,由于 arrayFields 指定了这两个字段名,输出依然能保持一致的数组结构 ["js"] 或 [],后端反序列化时不容易因类型变化而报错。
三、保留数字、布尔值与未勾选状态:遍历 form.elements
FormData 还有一个本质特点:所有非文件字段拿到的值都是字符串。例如 <input type="number" name="age" value="25"> 取出来的是 "25",单个用来表示开关的复选框(如“同意协议”)在未勾选时甚至不会出现在 FormData 里。
如果你的接口严格区分字符串、数字和布尔类型,或者需要把未勾选的单个复选框明确序列化为 false,直接遍历原生表单的 form.elements 集合会更合适。
遍历 form.elements 时要格外留意三类不需要进入 JSON 的元素:没有 name 的元素、设置了 disabled 的元素,以及提交按钮(submit)、普通按钮(button)、重置按钮(reset)和 <fieldset> 容器。另外,单选按钮组(radio)在遍历时会遇到多个同名控件,只能写入 el.checked 为 true 的那一个。
下面是一个处理单选框、单个开关复选框、多选复选框组、数字输入框以及多选下拉框的完整示例:
<form id="orderForm">
<input type="text" name="product" value="机械键盘" />
<input type="number" name="quantity" value="2" />
<label><input type="radio" name="payType" value="online" checked /> 在线支付</label>
<label><input type="radio" name="payType" value="cod" /> 货到付款</label>
<label><input type="checkbox" name="needInvoice" /> 需要发票</label>
<button type="submit">下单</button>
</form>
<script>
function serializeElementsToJson(form) {
const payload = {};
const elements = Array.from(form.elements);
for (const el of elements) {
if (!el.name || el.disabled) continue;
if (['button', 'submit', 'reset', 'fieldset', 'file'].includes(el.type)) continue;
if (el.type === 'radio') {
if (el.checked) {
payload[el.name] = el.value;
}
continue;
}
if (el.type === 'checkbox') {
// 判断表单中是否存在同名的多个复选框
const sameNameCheckboxes = form.querySelectorAll(
`input[type="checkbox"][name="${CSS.escape(el.name)}"]`
);
if (sameNameCheckboxes.length === 1) {
payload[el.name] = el.checked;
} else {
if (!Array.isArray(payload[el.name])) {
payload[el.name] = [];
}
if (el.checked) {
payload[el.name].push(el.value);
}
}
continue;
}
if (el.type === 'select-multiple') {
payload[el.name] = Array.from(el.selectedOptions).map((opt) => opt.value);
continue;
}
if (el.type === 'number') {
payload[el.name] = el.value === '' ? null : el.valueAsNumber;
continue;
}
payload[el.name] = el.value;
}
return JSON.stringify(payload);
}
const orderForm = document.getElementById('orderForm');
orderForm.addEventListener('submit', function (event) {
event.preventDefault();
console.log(serializeElementsToJson(orderForm));
});
</script>
未勾选“需要发票”直接提交时,控制台中会输出如下内容(示例输出):
// 示例输出
{"product":"机械键盘","quantity":2,"payType":"online","needInvoice":false}
可以看到,quantity 保持了数字类型 2,未勾选的 needInvoice 也明确记录为布尔值 false。
四、把序列化后的 JSON 发送到服务端
把表单数据序列化为 JSON 字符串后,通常需要配合 fetch() 发送给后端接口。这里最容易漏掉的是请求头中的 Content-Type:如果不显式指定 application/json,fetch 在发送字符串时默认使用 text/plain;charset=UTF-8,很多后端框架不会把它当作 JSON 解析。
const userForm = document.getElementById('userForm');
userForm.addEventListener('submit', async function (event) {
event.preventDefault();
const jsonString = serializeFormToJson(userForm, ['hobby']);
const response = await fetch('/api/profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: jsonString
});
if (!response.ok) {
throw new Error(`提交失败,状态码:${response.status}`);
}
const result = await response.json();
console.log(result);
});
Content-Type: application/json,就说明序列化与发送格式已经正确。
五、文件上传与嵌套字段的边界说明
有两类表单场景不适合直接套用上面的 JSON 序列化逻辑,提前识别可以避免走弯路:
1. 包含文件选择器(input[type="file"])的表单
FormData 可以直接采集用户选中的文件对象(File 实例),但如果把包含 File 实例的对象交给 JSON.stringify(),File 对象没有可枚举的自有数据属性,序列化结果会变成空对象 {},文件内容完全丢失。
当表单需要上传文件时,推荐直接把 new FormData(form) 传给 fetch 的 body(且不要手动设置 Content-Type,让浏览器自动带上边界标识),而不是转成 JSON。只有在接口明确要求把小文件编码为 Base64 放进 JSON 时,才需要另外用 FileReader 异步读取文件内容。
2. 带点号或方括号的嵌套字段名
无论是 Object.fromEntries() 还是简单的键值对遍历,都会把 name="user.address" 或 name="items[0]" 当作字面量键名,生成扁平对象 {"user.address": "..."},而不会自动拆分成 {"user": {"address": "..."}}。如果后端需要嵌套 JSON 对象,可以在序列化前按分隔符做一次路径拆分赋值,或者直接使用支持路径映射的表单库来处理。
六、方法选择与常见排查清单
实际开发中没有必要一上来就手写复杂的遍历逻辑,可以根据表单控件的复杂度按需选择最简单、够用的方案:
如果在实际使用中发现序列化后的 JSON 少了字段或者后端解析异常,可以对照下面几项快速排查:
| 常见现象 | 主要原因 | 解决方式 |
|---|---|---|
| 某个输入框的值完全没有出现在 JSON 中 | 控件缺少 name 属性,或设置了 disabled |
补上 name 属性;需要禁止修改时改用 readonly |
| 多选复选框勾选了多项,JSON 里只剩最后一项 | 直接使用了 Object.fromEntries() 导致同名键覆盖 |
改用遍历聚合函数或 formData.getAll(key) |
文件字段变成了 {} |
File 对象被传入了 JSON.stringify() |
改用 FormData 直接作为 fetch 的请求体上传 |
| 后端拿不到 JSON 字段或报解析错误 | 忘记设置 Content-Type: application/json 请求头,或未将对象转为字符串直接传给了 body |
确认 headers 包含 'Content-Type': 'application/json' 且 body 为 JSON.stringify() 的返回值 |
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。