侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 235 篇文章
  • 累计收到 7 条评论

URLSearchParams 不会展开对象里的数组

2026-10-24 / 0 评论 / 5 阅读

URLSearchParams 不会展开对象里的数组

把对象直接交给 URLSearchParams,看起来像是把一组参数交给了它。对象里的数组尤其容易让人误会:tag: ['alpha', 'beta'] 似乎应该变成两条 tag。本机跑出来的结果却只有一项,值是 alpha,beta。逗号不是分隔符,只是数组转成字符串后的普通字符。

对象初始化表达的是一组属性

对象的键本来就唯一,适合表达每个名字对应一个值。传入对象时,URLSearchParams 会把属性值转成字符串;它不会把数组解释成重复参数,也不会递归展开嵌套对象。下面这组本地输出来自 Node v26.8.1,脚本和原始日志分别保存在 tools/verify-urlsearchparams-object.mjs 与 tools/verify-urlsearchparams-object.log。

Node v26.8.1
object entries: [["tag","alpha,beta"],["repeated","object-value"],["nullable","null"],["nested","[object Object]"],["count","17"]]
object getAll(tag): ["alpha,beta"]
object getAll(repeated): ["object-value"]
ambiguous left: a,b,c
ambiguous right: a,b,c
pair entries: [["tag","alpha"],["tag","beta"]]
pair getAll(tag): ["alpha","beta"]
append entries: [["tag","alpha"],["tag","beta"],["repeated","first"],["repeated","second"]]
append getAll(tag): ["alpha","beta"]
append getAll(repeated): ["first","second"]
object serialized: tag=alpha%2Cbeta&repeated=object-value&nullable=null&nested=%5Bobject+Object%5D&count=17

null 也没有被当作缺省值跳过,而是变成字面字符串 null。嵌套对象则落成 [object Object]。这两种结果都能成功序列化,所以问题不一定会在构造时显形,服务端收到的可能只是语法上合法、含义却已偏离预期的查询字符串。可选字段若要省略,应该在构造前明确过滤;要传结构化内容,则应先选好编码方式,而不是把原始对象塞进来。

重复键需要明确写出来

用 append 连续加入相同名称,两个值会作为两条独立记录保留下来。getAll('tag') 返回两个元素,序列化结果也保有两个键值对。对象初始化中的 getAll('tag') 则只有一个值,内容中带逗号并不等于重复键。构造参数也可以是键值对序列,同样能保留两个 tag;本地输出中的 pair entries 与 append entries 都有两行。序列适合数据已经成对排列的情况,append 则适合边遍历边添加。两种方式都把重复关系写在数据结构里,不必再从值里的标点反推边界。

接收端若按多值参数读取,靠逗号拆分会把编码边界和业务分隔符混在一起;值本身一旦允许包含逗号,这个约定就失去可靠性。数组成员的边界也不会写进字符串里,['a,b', 'c'] 和 ['a', 'b,c'] 都可能变成同一段文本。序列化再编码逗号,只能确保 URL 中字符安全,不会补回数组边界。若使用逗号协议,双方还得约定转义规则和空元素的表示,这已经是另一套格式,不是 URLSearchParams 自动提供的能力。

当输入天然是一组键值对,而且允许同名键时,初始化参数应使用键值对序列,或者逐项调用 append。对象初始化并非错误,只是表达能力不同。它适合一键一值的映射,不适合暗示数组成员会被展开的场景。我的判断是,接口字段若定义为多值,宁愿在组装查询时保留多条记录,也不把数组先压成字符串再寄希望于另一端猜回原意。

转换发生在校验之前

这个差异也影响参数校验。若先把对象构造成 URLSearchParams,原始类型信息就已经丢失:数字和字符串都成为字符串,数组也退化成一个逗号连接的值。后续再检查 getAll 的长度,只能看到转换后的记录数,无法判断原输入曾经是数组、对象还是 null。校验应先针对输入结构完成,再按接口约定逐条追加或构造一对一的对象。

不能把 toString() 的结果当成还原输入的凭据。样例里的嵌套对象和 null 都产生了普通文本,查询串不会标记它们原先的类型。若协议确实需要一个值承载数组,可以选择 JSON 等明确格式并按同一协议解析;若协议把数组定义成多个同名键,就应让多个键从构造阶段一直保留到解析阶段。两者都可用,混用才会让调用方和接收方各自采用不同的解释。

重复键的语义应该由接口契约决定,而不是由对象字面量的外观决定。单值映射可以用对象初始化;多值参数用序列或 append 更清楚。这样,校验看到的数量与线上实际携带的数量才一致。

    🤞 分享