先记住这个答案
对普通数组,concat 与 [...a, ...b] 都能构造一个浅合并的新数组,不会递归拆开任意嵌套层级,内层对象和数组仍共享引用。concat 按自己的可展开规则处理参数,普通数组默认展开一层,并可受 Symbol.isConcatSpreadable 影响;数组展开则消费 Symbol.iterator。普通稀疏数组经 concat 可保留空位,经数组展开通常变成显式 undefined。因此不能把二者说成对所有输入完全等价。若需要深度展平,应明确使用 flat 的深度及其空位规则。
- 新外层容器仍共享嵌套对象与数组
- concat 与 spread 使用不同展开契约
- 稀疏结构是观察差异的直接用例
先确认合并后哪一层仍然共享
示例将一个包含内层数组的容器与另一数组合并,两种写法都保留同一个内层数组引用。后续修改内层时,两份结果都能看到变化,说明浅合并不是数据隔离操作。
第二组使用只有长度的稀疏数组,结果值打印可能相似,因此通过 Object.hasOwn 检查自有索引。这个断言比只看 JSON 更准确,因为 JSON 会把空位和 undefined 都编码成 null。
const nested = [1, 2];
const left = [nested];
const joined = left.concat([3]);
const spread = [...left, 3];
console.log(joined[0] === nested, spread[0] === nested);
nested.push(4);
console.log(JSON.stringify(joined));
const sparse = new Array(1);
console.log(Object.hasOwn([].concat(sparse), 0));
console.log(Object.hasOwn([...sparse], 0));查看输出与解释
true true
[[1,2,4],3]
false
true内层数组没有被克隆,增加四后合并结果同步看到变化。concat 保留缺失索引,而普通数组迭代器展开后写入显式 undefined,所以自有属性检查结果不同。
参数不是真数组时要看实际协议
Set 可以通过数组展开产生成员,但把 Set 直接传给 concat 通常只是追加这个对象本身。另一方面,一个带 length 和索引的对象可以用 Symbol.isConcatSpreadable 表达 concat 展开意图,却不因此自动拥有迭代器。
接口设计最好选定清晰输入类型,避免同时支持很多隐式展开约定。若接收外部集合,先按受控转换规则标准化,再合并,错误提示也更容易指向真正不符合约定的输入。
展平深度与复制深度是两件事
flat 可以按指定层数去掉数组嵌套,但结果中的对象依然是引用,并不等于深拷贝。需要把树形记录转成列表时,还应明确哪些层级代表结构、哪些数组本身是合法业务值。
大数据合并会分配新容器,反复在循环里 concat 不断增长的数组可能重复复制已有内容。应根据吞吐和内存证据选择一次构造或受控累积,不能只追求一行写法而忽略总分配。
容易答错的地方
- 把 concat 和 spread 说成完全相同的语法糖
- 两者对迭代器、展开标记和空位的处理不同,普通密集数组结果相同只是常见子集。替换时应检查输入契约和属性结构,尤其是自定义集合与稀疏数组。
- 认为新数组里的嵌套数据都已复制
- 浅合并只建立外层容器,内层引用仍可共享。若后续要修改嵌套内容,应复制必要层级或采用明确不可变更新,避免修改合并结果时意外改变输入。
面试官还会怎么问?
concat 会修改调用它的原数组吗?
普通数组 concat 返回新数组而不主动修改源容器,但读取访问器或自定义对象仍可能产生用户定义副作用。常规业务可按非修改合并理解,同时不要把它扩张为对任意对象绝无副作用。
为什么 Set 用 concat 没有展开成员?
concat 不按 Set 的默认迭代器收集成员,通常把它当作普通单项参数。需要成员列表时应显式展开或 Array.from 转换,让代码表达清楚想追加集合本身还是集合里的每个值。
要合并很多小数组怎样减少重复复制?
可以一次收集输入后合并,或在受控目标数组里逐项累积,再按需要冻结或发布结果。选择应考虑数据规模与可变约束,避免在每轮都复制越来越长的已有结果。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。