先记住这个答案
对普通数组来说,a.concat(b) 和 [...a, ...b] 都会创建一个新数组,都只拆开最外层,内层的数组和对象仍然和原数组共享引用,所以两者都是浅合并。区别出在非普通数组的输入上:concat 只拆数组,传入 Set、字符串、普通对象都整体追加;展开语法走迭代器协议,可迭代的都会被拆开,不可迭代的直接报错。另外 concat 会保留稀疏数组的空位,展开则把空位读成 undefined。
- 两者都只拆一层,都是浅拷贝
concat只展开数组参数,其它值原样追加- 展开按迭代器取值,
Set和字符串也会被拆开 concat保留空位,展开把空位变成undefined
普通数组:结果一样,都是浅的
先看最常见的情况,合并两个普通数组,其中有嵌套数组和对象。
const inner = { n: 1 }
const a = [1, [2, 3]]
const b = [inner]
const c1 = a.concat(b, 4) // 4 不是数组,直接追加
const c2 = [...a, ...b, 4]
console.log(JSON.stringify(c1), JSON.stringify(c2))
inner.n = 99 // 改原对象
console.log(c1[2].n, c2[2].n) // 两个新数组里都变了
console.log(c1[1] === a[1]) // 内层数组是同一个查看输出与解释
[1,[2,3],{"n":1},4] [1,[2,3],{"n":1},4]
99 99
true[2, 3] 没有被拆开,因为两种方式都只处理传进来的那一层。新数组像一个新的书架,书还是原来那几本,谁在上面做了批注,另一个书架上的也跟着变。要彻底断开引用,得用 structuredClone 或者自己递归复制。
非普通数组:拆不拆、怎么拆不一样
concat 判断的是「这个参数是不是数组」,展开判断的是「这个值能不能迭代」。标准不同,遇到边界输入结果就不同了。
Set 和字符串JavaScriptconst sparse = [1, , 3] // 下标 1 是空位,不是 undefined
const s = new Set([4, 5])
const x = [].concat(sparse)
const y = [...sparse]
console.log(1 in x, 1 in y) // concat 保留空位,展开变成真实的 undefined
console.log([].concat(s).length, [...s].length) // Set 整体追加 vs 拆开
console.log([].concat('ab').length, [...'ab'].length) // 字符串同理查看输出与解释
false true
1 2
1 2空位指数组在这个下标上根本没有属性,1 in x 是 false。而 concat 复制时会先检查属性是否存在,不存在就跳过,所以空位保留了下来。展开是通过迭代器一个个取值的,迭代器读到空位就返回 undefined,于是 y 的下标 1 上真的有了一个 undefined。这个差别会影响后面的 forEach、map,它们会跳过空位,但不会跳过 undefined。
- 用
Symbol.isConcatSpreadable改变concat的行为 concat其实是看参数的Symbol.isConcatSpreadable属性,没设置时才退回到「是不是数组」。给一个类数组对象设置这个属性为true,那么concat就会按length和下标拆开它;给数组设置为false,它就会被整体追加。日常业务很少用,但它解释了concat为什么能区别对待不同参数。
容易答错的地方
- 以为
concat会把嵌套数组全部拍平 [1].concat([[2, [3]]])得到的是[1, [2, [3]]],只拆了参数本身那一层。要拍平多层用flat(depth),比如flat(Infinity)拍到底。- 以为展开出来的是深拷贝
[...arr]常被用来复制数组,但复制的只是外层。在 React 里用展开复制状态数组后直接改里面的对象,原状态也被改了,组件可能不会重新渲染。- 以为展开能处理任何对象
- 数组字面量里的展开要求值可迭代。
[...{ length: 2 }]会抛TypeError,因为普通对象没有Symbol.iterator。类数组对象要先用Array.from转换。
面试官还会怎么问?
往已有数组追加一大批元素,push(...big) 有什么问题?
push(...big) 会把每个元素都变成一个函数参数,数组有几十万项时可能超出引擎的参数数量上限,抛 RangeError。追加大数组可以分批 push,或者直接用 concat 生成新数组。
concat 和展开哪个更快?
不同引擎、不同数组大小结论不一样,没有固定答案。合并普通数组时两者差别通常可以忽略,按可读性选就行,真遇到性能热点再用实际数据测。
合并时想去重怎么写?
[...new Set([...a, ...b])]。先合并,再交给 Set 去重,最后展开回数组。Set 判断相等用的是 SameValueZero,NaN 能被正确去重,但两个内容相同的对象会被当成不同元素。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。