把HTML表单提交给后端接口时,jQuery提供了serialize()方法能把所有成功控件编码成查询字符串,但如果接口只接收JSON格式的请求体,就需要将表单数据进一步整理成对象。借助jQuery的$.map()函数和serializeArray()方法,可以在遍历控件数据的过程中完成字段映射、重复值合并以及类型转换,最终得到可直接交给$.ajax发送的JSON对象。下面围绕这个转换过程展开分析。

一、理解serializeArray()返回的数据结构
serializeArray()是jQuery表单处理中一个很实用的方法。它会把<form>内部所有符合条件的控件读取出来,生成一个由普通对象组成的数组。数组中的每一项都包含两个属性:name和value,分别对应控件的名称和当前值。例如一个包含用户名、角色和标签多选的表单,调用$('#userForm').serializeArray()后可能得到类似这样的结构:
[
{ name: 'username', value: 'tom' },
{ name: 'role', value: 'admin' },
{ name: 'tags', value: 'frontend' },
{ name: 'tags', value: 'backend' }
]
与serialize()相比,serializeArray()保留了每个控件的独立名称和值,不会提前拼成字符串,这为后续转换成JSON对象提供了很大便利。但需要注意的是,它只会包含成功控件,也就是未被禁用、带有name属性、并且在当前表单中有效提交的控件。未勾选的复选框、未选中的单选按钮以及没有name属性的按钮,都不会出现在返回结果中。这一点在后续处理布尔字段和空值字段时需要特别留意。
jQuery中的$.map()函数可以遍历数组或对象,并对每一项执行回调函数,最后把回调返回的值组成一个新数组。它的回调参数顺序与$.each()相反,第一个参数是数组元素,第二个参数是索引。因此很适合读取serializeArray()数组中的name和value。不过$.map()本身不会直接修改原数组,需要我们在回调中构建目标对象,或者使用它返回的新数组完成进一步计算。
var fields = $('#userForm').serializeArray();
var fieldNames = $.map(fields, function(field) {
return field.name;
});
console.log(fieldNames);
二、用$.map()把serializeArray()结果转换为JSON对象
最基础的转换思路是创建一个空对象,然后利用$.map()遍历serializeArray()得到的数组,在回调里把field.name作为对象的键,把field.value作为值写入。由于$.map()的回调总会执行,副作用写入外部对象并没有问题,只是返回值通常会被忽略。如果表单里没有重名字段,这样写已经可以工作。
var fields = $('#userForm').serializeArray();
var json = {};
$.map(fields, function(field) {
json[field.name] = field.value;
});
console.log(JSON.stringify(json));
但真实场景中,复选框组和多选下拉框会提交多个同名值,上面的代码会让后面的值覆盖前面的值。例如三个<input type='checkbox' name='tags'>同时选中时,serializeArray()会返回三条tags记录,但最终对象里只会留下最后一个值。要解决这个问题,需要在回调里判断当前键是否已经存在:如果不存在就直接赋值;如果已经存在,就把原来的值包装成数组,再追加新值。这样最终得到的tags字段就是一个包含所有勾选项的数组。
function formToJson($form) {
var data = $form.serializeArray();
var result = {};
$.map(data, function(field) {
var name = field.name;
var value = field.value;
if (Object.prototype.hasOwnProperty.call(result, name)) {
if (!$.isArray(result[name])) {
result[name] = [result[name]];
}
result[name].push(value);
} else {
result[name] = value;
}
});
return result;
}
判断已有键时使用了Object.prototype.hasOwnProperty.call,这样可以避免原型链上同名属性带来的干扰。同时用$.isArray()判断当前值是否已经转为数组,如果不是数组,说明之前只存了一个字符串,就先包装成数组。之后无论再遇到多少个同名控件,都直接追加到这个数组中。这样处理后,普通文本框仍然保持字符串,多选控件则会得到数组,符合大多数后端接口的预期。
还需要注意,serializeArray()返回的值都是字符串。如果表单里有一个<input type='number' name='age'>,用户输入28,转换后得到的age仍然是字符串'28'而不是数字28。后端如果用强类型语言接收JSON,数字字段可能会因为类型不匹配而报错。因此根据业务需要做类型转换非常有必要,这个会在下一节详细说明。
三、类型转换、嵌套字段与文件输入的边界
仅做简单的键值映射还不足以应对复杂表单。很多Web应用会使用data-type属性来标识某个字段应该被转成数字、布尔值或JSON对象。转换逻辑可以在$.map()的回调里读取原始控件,根据约定的属性返回对应类型的值。例如给年龄输入框加上data-type='number',给开关加上data-type='boolean',这样序列化结果就能直接包含数值和布尔值,而不再是一堆字符串。
function convertValue(field, value) {
var type = field.attr('data-type');
if (type === 'number') {
return Number(value);
}
if (type === 'boolean') {
return value === 'true' || value === '1';
}
if (type === 'json') {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}
return value;
}
另一类常见需求是名字里带方括号的嵌套字段,比如user[name]、user[email]、address[city]。这类命名方式在PHP等后端语言中很常见,但JSON接口通常希望得到嵌套对象。可以通过一个辅助函数把user[name]拆分成user和name两级,再逐级创建对象。处理时要先把右方括号去掉,再把左方括号作为分隔符拆分,这样就能得到一个键路径数组。
function setNestedValue(obj, path, value) {
var keys = path.replace(/\]/g, '').split('[');
var current = obj;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (i === keys.length - 1) {
current[key] = value;
} else {
current[key] = current[key] || {};
current = current[key];
}
}
return obj;
}
这里path.replace(/\]/g, '')会把字符串中的右方括号全部去掉,split('[')再按左方括号切分。对于user[name],keys数组就是['user', 'name'];对于普通字段username,keys数组只有['username']。循环处理时,最后一层直接赋值,前面的层级如果不存在就创建空对象。这样无论是简单字段还是嵌套字段都能统一处理。
文件输入是表单序列化中的另一个边界。<input type='file'>无法通过serializeArray()读取到二进制内容,因为安全限制和编码方式的原因,文件只能使用FormData对象来携带。如果表单里包含文件上传,建议单独判断:当存在type='file'的控件时,改用new FormData(formDom)来构造请求体,并且不要手动设置contentType,让浏览器自动添加multipart/form-data边界。纯文本表单则继续使用$.map()方案生成JSON。
四、封装通用的formToJson工具并做边界处理
把上面的逻辑整合成一个可以在项目中复用的jQuery插件,能减少重复代码。插件最好支持一些常见配置,比如是否把空字符串转为null、是否自动读取data-type进行类型转换。通过$.extend()合并默认配置和用户配置,然后在this.map()中遍历每个匹配的表单元素,返回序列化后的对象数组。
$.fn.formToJson = function(options) {
var settings = $.extend({
emptyToNull: false,
parseTypes: true
}, options);
return this.map(function() {
var $form = $(this);
var data = $form.serializeArray();
var result = {};
$.map(data, function(field) {
var name = field.name;
var value = field.value;
var $field = $form.find('[name="' + name + '"]');
if (settings.parseTypes) {
value = convertValue($field.first(), value);
}
if (settings.emptyToNull && value === '') {
value = null;
}
setNestedValue(result, name, value);
});
return result;
}).get();
};
上面的插件调用方式为$('#userForm').formToJson(),返回一个对象数组。如果页面中只匹配一个表单,通常取数组第一项即可;如果同时处理多个表单,也可以按索引获取。这里使用this.map()而不是直接返回单个对象,是考虑到jQuery选择器可能匹配多个表单的情况。它会把每个表单都转换成一个JSON对象,最后统一返回。
边界情况需要重点排查。被禁用的控件不会出现在serializeArray()中,因此不会进入JSON。未勾选的复选框和未选中的单选按钮同样不会出现,如果后端要求明确的布尔值字段,需要额外在提交前补默认值。多选下拉框<select multiple>会出现多个同名项,插件已经处理为数组。文本框中的换行符会被保留在字符串值里,一般无需额外转义。对于包含大量空格或纯空格的输入,是否转为null取决于配置,开启emptyToNull后会更符合REST接口的语义。
与原生FormData相比,$.map()方案的优势在于数据结构完全可控,可以自由调整字段名称、合并重复项、转换类型,生成的就是标准JavaScript对象,方便JSON.stringify()后通过$.ajax发送。与现代前端框架中的受控组件相比,jQuery方案更适合维护存量项目,不需要引入额外依赖,逻辑也相对直观。当然,如果项目已经全面使用React或Vue,通常不再需要手动序列化表单,因为组件状态本身就是对象。但在传统服务端渲染页面和旧版jQuery代码中,这种转换仍然非常实用。
整体来看,使用$.map()把serializeArray()结果转换为JSON对象,核心就是遍历数组、处理重名字段、根据业务规则转换类型,再通过辅助函数支持嵌套结构。把这些逻辑封装成插件后,可以在多个表单中复用,减少序列化相关的重复代码。对于不包含文件上传的常规表单来说,这是一种稳定且易于维护的解决方案。

