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() 是更稳妥的选择。

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

参考

相关推荐
naruto_lnq1 天前
分布式系统安全通信
开发语言·c++·算法
Mr Xu_1 天前
告别冗长 switch-case:Vue 项目中基于映射表的优雅路由数据匹配方案
前端·javascript·vue.js
前端摸鱼匠1 天前
Vue 3 的toRefs保持响应性:讲解toRefs在解构响应式对象时的作用
前端·javascript·vue.js·前端框架·ecmascript
学嵌入式的小杨同学1 天前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
sleeppingfrog1 天前
zebra通过zpl语言实现中文打印(二)
javascript
Re.不晚1 天前
Java入门17——异常
java·开发语言
精彩极了吧1 天前
C语言基本语法-自定义类型:结构体&联合体&枚举
c语言·开发语言·枚举·结构体·内存对齐·位段·联合
南极星10051 天前
蓝桥杯JAVA--启蒙之路(十)class版本 模块
java·开发语言
未来之窗软件服务1 天前
未来之窗昭和仙君(六十五)Vue与跨地区多部门开发—东方仙盟练气
前端·javascript·vue.js·仙盟创梦ide·东方仙盟·昭和仙君
baidu_247438611 天前
Android ViewModel定时任务
android·开发语言·javascript