
表单里同一个字段有时会出现多个值,例如多选项或重复上传的文件。FormData.append() 会把新值放到后面;改值时常会换成 set()。一个不太显眼的差异是:set() 除了覆盖内容,还会改变键值对的排列吗?如果后面要遍历表单生成签名或展示顺序,顺序就不是装饰。
这次用 Node 自带的 FormData 连续插入五项,其中 item 出现两次,中间隔着 middle。之后对已有的 item 调用 set(),再对一个不存在的名字调用一次。结果很清楚:已有名字被换成一个新值,位置仍是第一次出现的位置;同名的后续值消失了。新名字则出现在队尾。
const form = new FormData();
form.append('before', 'B');
form.append('item', 'old-1');
form.append('middle', 'M');
form.append('item', 'old-2');
form.append('after', 'A');
form.set('item', 'new');
console.log([...form.entries()]);
输出不等于删除所有 item 后再把新值追加。item 仍排在 before 后面,middle 跟在它后面。这个位置对应原来第一个同名条目,而不是最后一个。因而,若调用者把 FormData 转成数组再按顺序处理,替换操作不会把字段悄悄移到最末端。
保留的是首次出现的位置
脚本输出的原始条目如下。完整结果里既有替换已有字段,也有新增字段的情况。
initial entries:
["before","B"]
["item","old-1"]
["middle","M"]
["item","old-2"]
["after","A"]
after set existing key:
["before","B"]
["item","new"]
["middle","M"]
["after","A"]
after set missing key:
["before","B"]
["item","new"]
["middle","M"]
["after","A"]
["missing","N"]
assertions: passed
注意 set() 处理已有名字时,不是只改第一个值、让第二个重复值留着。输出里第二个 item 完全消失。调用后的条目数量从五项变成四项,新增一个 missing 后才回到五项。依赖重复字段时,这个差异比顺序更容易造成数据丢失:set() 的语义是设置单值,不是更新某一个重复项。
这也说明 append() 和 set() 不能只按「加」和「改」来记。前者追加一个条目,即使名字已经存在也保留旧项;后者对已有名字进行归并,留下一个新值,并把它安放在首个旧条目所在位置。调用时若想保留多个值,应继续用 append(),而不是对同名字段反复 set()。
新字段才会走到队尾
对尚未存在的名字调用 set(),结果与新增一项相同,放在现有条目之后。因而连续调用 set() 给新字段赋值,其顺序由这些调用进入表单的先后决定。对旧字段的重设则不会把它挪到新字段后面。
这个差别会影响把表单顺序序列化成字符串的代码。不过,不能把本机 FormData 的迭代顺序直接当作所有后续系统的协议保证。服务端框架可能把重复字段折叠成单值,也可能把它们转成数组;签名算法还可能自行排序。若顺序参与摘要或验签,双方需要约定规范化规则,不能靠浏览器里刚好看到的排列来猜。
实验运行于当前机器的 Node 内置实现,验证的是 FormData 条目与迭代顺序,不涉及真实浏览器提交或服务端解析。不同浏览器之间的实现差异尚未比较,文件对象在网络传输后的字段排序也没有验证。项目若需要跨环境保证,应把最终序列化结果作为契约测试,而不是只测内存中的 entries()。这个边界得留在结论里,不能把本机结果扩写成浏览器兼容承诺。
我的取舍是:重复值表示「多选」或「多文件」时,坚持用 append();确实需要把一个字段收敛为单值时才用 set()。替换旧值保留首次位置,适合保持既有表单结构,但不等于重复值能安全保留。看到同名项时,先确认要的是列表还是单值,再选操作。