JavaScript 数组合并性能优化:扩展运算符 vs concat vs 循环 push

在日常开发中,我们经常需要合并数组,比如批量导入数据、分页加载列表、处理大量日志等场景。当数组规模较小时,用什么方法都差不多;但当数组达到成千上万条时,选择不当的方法可能会导致栈溢出内存飙升

今天我们就来聊聊 JavaScript 中三种常见的数组合并方式,看看它们各自的优缺点,以及在不同场景下应该如何选择。

三种数组合并方式

1. 扩展运算符 + push:arr.push(...otherArr)

这是最"爽"的写法,一行代码搞定:

javascript 复制代码
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
arr1.push(...arr2)
console.log(arr1) // [1, 2, 3, 4, 5, 6]

优点:

  • 代码简洁,可读性强
  • 语法现代,符合 ES6+ 风格
  • 适合小数组快速合并

缺点:

  • 当数组元素超过约 65536 个时,会触发 JavaScript 引擎的最大参数数量限制,导致栈溢出错误
  • 扩展运算符需要先展开数组,会消耗额外内存
  • 大数组性能较差

适用场景:

  • 小数组(< 1000 个元素)
  • 快速脚本开发
  • 数量可控的场景

2. concat 方法:arr.concat(otherArr)

这是最"安全"的写法:

javascript 复制代码
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const merged = arr1.concat(arr2)
console.log(merged) // [1, 2, 3, 4, 5, 6]

优点:

  • 不会导致栈溢出,适合大数组
  • 语义清晰,代码可读性好
  • 返回新数组,不修改原数组(函数式编程友好)
  • 性能稳定,适合生产环境

缺点:

  • 会创建新数组,占用额外内存
  • 对于超大数组,内存占用会翻倍

适用场景:

  • 中大数组(> 1000 个元素)
  • 需要保持原数组不变
  • 函数式编程风格
  • 生产环境安全要求高

3. 循环 push:for (const item of arr) { target.push(item) }

这是最"稳"的写法:

javascript 复制代码
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
for (const item of arr2) {
  arr1.push(item)
}
console.log(arr1) // [1, 2, 3, 4, 5, 6]

优点:

  • 最安全,绝对不会栈溢出
  • 内存占用最低(原地修改,不创建新数组)
  • 性能稳定,适合超大数组
  • 兼容性最好

缺点:

  • 代码相对冗长
  • 需要手动写循环
  • 会修改原数组

适用场景:

  • 超大数组(> 10000 个元素)
  • 内存敏感场景
  • 长时间运行的服务端任务
  • 需要最高性能的场景

性能对比表

方法 栈溢出风险 内存占用 性能 代码简洁度 适用数组大小
push(...arr) ⚠️ 高(>65536) 差(大数组) ⭐⭐⭐⭐⭐ < 1000
concat() ✅ 无 中高 ⭐⭐⭐⭐ > 1000
循环 push() ✅ 无 最好 ⭐⭐⭐ > 10000

实际案例对比

让我们看看在不同规模下的表现:

小数组(100 个元素)

javascript 复制代码
// 三种方法都可以,性能差异可忽略
const small = Array.from({ length: 100 }, (_, i) => i)

// 方法1:扩展运算符
arr1.push(...small) // ✅ 推荐

// 方法2:concat
const merged = arr1.concat(small) // ✅ 也可以

// 方法3:循环
for (const item of small) arr1.push(item) // ✅ 也可以,但没必要

中数组(10000 个元素)

javascript 复制代码
const medium = Array.from({ length: 10000 }, (_, i) => i)

// 方法1:扩展运算符
arr1.push(...medium) // ⚠️ 可能栈溢出!

// 方法2:concat
const merged = arr1.concat(medium) // ✅ 推荐

// 方法3:循环
for (const item of medium) arr1.push(item) // ✅ 也可以

大数组(100000 个元素)

javascript 复制代码
const large = Array.from({ length: 100000 }, (_, i) => i)

// 方法1:扩展运算符
arr1.push(...large) // ❌ 几乎肯定会栈溢出!

// 方法2:concat
const merged = arr1.concat(large) // ⚠️ 可以,但内存占用高

// 方法3:循环
for (const item of large) arr1.push(item) // ✅ 强烈推荐

进阶优化技巧

1. 分批处理超大数组

当数组特别大时,可以分批处理,避免一次性操作:

javascript 复制代码
function batchConcat(target, source, batchSize = 5000) {
  for (let i = 0; i < source.length; i += batchSize) {
    const batch = source.slice(i, i + batchSize)
    target.push(...batch) // 小批次可以用扩展运算符
  }
  return target
}

// 使用
const huge = Array.from({ length: 100000 }, (_, i) => i)
const result = []
batchConcat(result, huge) // 安全处理超大数组

2. 使用 Array.from 和展开运算符组合

对于需要转换的场景:

javascript 复制代码
// 不推荐:可能栈溢出
const mapped = arr.map(x => x * 2)
result.push(...mapped)

// 推荐:使用 concat
const mapped = arr.map(x => x * 2)
result = result.concat(mapped)

// 或者:直接循环
for (const item of arr) {
  result.push(item * 2)
}

3. 流式处理(迭代器)

如果数据来自流或生成器,可以边读边处理:

javascript 复制代码
async function* fetchData() {
  // 模拟分页数据流
  for (let i = 0; i < 10; i++) {
    yield await fetchPage(i)
  }
}

const result = []
for await (const page of fetchData()) {
  // 流式处理,内存占用低
  result.push(...page) // 每页数据量小,可以用扩展运算符
}

常见错误示例

❌ 错误示例 1:批量导入时使用扩展运算符

javascript 复制代码
// 批量导入 Excel,可能有上万行数据
const excelData = []
sheets.forEach((sheet) => {
  const current = xlsx.utils.sheet_to_json(workbook.Sheets[sheet])
  excelData.push(...current) // ❌ 如果数据量大,会栈溢出!
})

正确做法:

javascript 复制代码
const excelData = []
sheets.forEach((sheet) => {
  const current = xlsx.utils.sheet_to_json(workbook.Sheets[sheet])
  // ✅ 使用 concat 或循环
  excelData = excelData.concat(current)
  // 或者
  // for (const row of current) excelData.push(row)
})

❌ 错误示例 2:分页加载时使用扩展运算符

javascript 复制代码
// 分页加载,累计可能有大量数据
const loadMore = async () => {
  const newData = await fetchData(offset)
  allData.push(...newData) // ❌ 随着数据累积,可能栈溢出
}

正确做法:

javascript 复制代码
const loadMore = async () => {
  const newData = await fetchData(offset)
  // ✅ 使用 concat
  allData = allData.concat(newData)
  // 或者
  // for (const item of newData) allData.push(item)
}

最佳实践建议

  1. 小数组(< 1000) :随意,push(...arr) 最简洁
  2. 中数组(1000 - 10000) :优先使用 concat(),安全可靠
  3. 大数组(> 10000) :使用循环 push(),性能最优
  4. 不确定大小时 :保守选择 concat() 或循环 push()
  5. 需要保持原数组不变 :使用 concat()
  6. 内存敏感场景 :使用循环 push(),原地修改

总结

数组合并看似简单,但在处理大规模数据时,选择合适的方法至关重要:

  • 扩展运算符 push(...arr):适合小数组,代码简洁,但大数组会栈溢出
  • concat():适合中大数组,安全可靠,但会创建新数组
  • 循环 push():适合超大数组,性能最优,内存占用最低

记住一个原则:不确定数组大小时,选择更安全的方法 。在生产环境中,concat() 和循环 push() 是更稳妥的选择。

希望这篇文章能帮助你在实际开发中避免栈溢出的坑!如果觉得有用,欢迎点赞收藏~

参考

相关推荐
打工的小王2 小时前
java并发编程(三)CAS
java·开发语言
油丶酸萝卜别吃2 小时前
Mapbox GL JS 表达式 (expression) 条件样式设置 完全指南
开发语言·javascript·ecmascript
爱吃大芒果2 小时前
Flutter for OpenHarmony前置知识:Dart 语法核心知识点总结(下)
开发语言·flutter·dart
Ulyanov2 小时前
从桌面到云端:构建Web三维战场指挥系统
开发语言·前端·python·tkinter·pyvista·gui开发
星火开发设计2 小时前
C++ 函数定义与调用:程序模块化的第一步
java·开发语言·c++·学习·函数·知识
cypking2 小时前
二、前端Java后端对比指南
java·开发语言·前端
摘星编程2 小时前
用React Native开发OpenHarmony应用:timing定时动画参数
javascript·react native·react.js
钟离墨笺3 小时前
Go语言--2go基础-->map
开发语言·后端·golang
JosieBook3 小时前
【Vue】12 Vue技术—— Vue 事件修饰符详解:掌握事件处理的高级技巧
前端·javascript·vue.js