前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库数组链式 map filter 中间内存分配
JavaScript数组与集合

map、filter、sort 链式调用会创建几个数组?怎么减少中间分配?

map 和 filter 各创建一个新数组,sort 在调用它的数组上原地排序,返回的还是那个数组,所以 map().filter().sort() 一共新建两个数组。要减少分配,可以把映射和过滤合进一个循环,但回调的执行顺序会跟着变,先确认结果等价,再用真实数据测有没有必要。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#数组与集合
先看核心答案读代码示例
理解线索

链式调用里的三类分配

  1. 结果数组map、filter、slice、toSorted 每调用一次就新建一个
  2. 原地操作sort、reverse、splice 不新建数组,但会修改调用它的那个数组
  3. 回调里的分配map((u) => ({ ...u })) 每项都新建对象,往往比外层数组更占内存

中间数组在链式调用结束后就没有引用了,会被垃圾回收。它的代价主要是分配和回收的时间,只有数据量大或者调用非常频繁时才明显。

核心回答

先记住这个答案

map 和 filter 都会返回新数组,sort 不会,它在调用它的数组上原地排序,然后返回同一个数组。所以 arr.map(f).filter(g).sort(h) 新建了两个数组,不是三个,而原数组 arr 不受影响。除了外层数组,回调里新建的对象、字符串也是分配,而且常常比外层数组更多。如果映射和过滤都是纯函数,可以合并成一个循环直接写出最终结果,少一个中间数组;但这样会改变回调的执行顺序,有副作用时结果可能不一样。所以先确认结果等价,再在真实数据上测,确定是热点才改。

  • map、filter 返回新数组,sort 原地修改
  • 直接对源数组 sort 会把源数据改掉
  • 合并成一个循环能省中间数组,但回调顺序会变
  • reduce 里写 [...acc, x] 是 O(n²) 的分配

数一数到底创建了几个数组

用 === 比较每一步的返回值和上一步,就能看出哪一步新建了数组。

每一步返回的是不是新数组JavaScript
const src = [3, 1, 2, 4]
const a = src.map((x) => x * 10) // 新数组
const b = a.filter((x) => x > 10) // 新数组
const c = b.sort((x, y) => x - y) // 原地排序,返回 b 本身

console.log(a === src, b === a, c === b)
console.log(JSON.stringify(src), JSON.stringify(c))
查看输出与解释
false false true
[3,1,2,4] [20,30,40]

c === b 是 true,说明 sort 没有新建数组。这里原数组 src 没被改,是因为 sort 作用在 filter 产生的新数组上。如果链的第一步就是 sort,比如 list.sort(...).map(...),被排序的就是 list 本身,React 状态、props、接口缓存都可能被悄悄改掉。需要不修改原数组的排序,用 ES2023 的 toSorted,它会返回一个排好序的新数组。

合并成一个循环会改变什么

把 map 和 filter 合并成一个循环,常被叫作循环融合。它省掉了中间数组,但执行顺序从「全部映射完再全部过滤」变成了「每一项映射完马上过滤」。

链式调用和融合循环的回调顺序JavaScript
const nums = [1, 2, 3]
const log = []
nums.map((x) => { log.push('map' + x); return x * 2 })
  .filter((y) => { log.push('filter' + y); return y > 2 })
console.log(log.join(' '))

const log2 = []
const out = []
for (const x of nums) {
  log2.push('map' + x); const y = x * 2
  log2.push('filter' + y); if (y > 2) out.push(y)
}
console.log(log2.join(' '), JSON.stringify(out))
查看输出与解释
map1 map2 map3 filter2 filter4 filter6
map1 filter2 map2 filter4 map3 filter6 [4,6]

两种写法最终结果相同,但回调的交错顺序完全不同。回调是纯函数时这无所谓;如果回调里有计数、打点、读写外部状态,融合后结果就可能变了。另外,map 和 filter 会跳过空位、会给回调传第三个参数(原数组),手写循环要把这些也考虑进去,否则就不是等价改写。

什么时候值得这样做
几百几千条数据的列表,中间数组的开销基本感知不到,保持链式写法更易读。值得优化的通常是这几种情况:每帧都要执行的逻辑,比如动画、滚动、拖拽时的计算;数据量到了几十万条;或者性能面板里确实能看到这段代码的垃圾回收占比很高。先用 Performance 面板或 Memory 面板确认,再动手。
回答前,多想一步

容易答错的地方

以为链式调用每一步都会复制一份
sort、reverse 是原地操作,不新建数组。反过来,它们会修改调用它们的数组,这是比多分配一个数组更常见的 bug 来源。
在 reduce 里展开累加器
arr.reduce((acc, x) => [...acc, x * 2], []) 每一步都新建一个数组并把之前的元素全部复制过去,数组长度为 n 时总共复制约 n²/2 次,是 O(n²) 的。数据量一大,它会比 map 慢几个数量级。在 reduce 里应该直接 acc.push(x) 再 return acc。
只盯着外层数组
users.map((u) => ({ ...u, label: u.name + u.id })) 每项都新建了一个对象和一个字符串。一万个用户就是一万个对象,外层数组只多了一个。优化内存时要先看回调里在造什么。
试着用自己的话回答

面试官还会怎么问?

toSorted 和 sort 怎么选?

需要保留原数组时用 toSorted,比如排序 React 状态或者 props;数组本来就是你自己新建的临时数组,用 sort 原地排序就行,少一次复制。toReversed、toSpliced、with 也是同一批加入的不修改原数组的版本。

有没有办法保持链式写法,又不产生中间数组?

ES2025 加入了迭代器辅助方法,可以写 arr.values().map(f).filter(g).toArray()。迭代器是惰性的,每一项依次经过 map 和 filter,只在最后 toArray 时生成一个数组。要注意目标环境是否支持,老环境需要 polyfill。

怎么确认这段代码真的有分配问题?

在 Chrome DevTools 的 Memory 面板用 Allocation sampling 录一段操作,看分配最多的函数;或者在 Performance 面板里看 Minor GC 是否频繁。改完后用同样的数据再测一次对比,没有明显变化就不值得牺牲可读性。

从一道题,走向一组知识

把知识连起来

数组与集合

数组头部 shift、unshift 为什么常比尾部 push、pop 成本高?

shift 每次都要把后面的元素整体前移,是和中间数组并列的另一种隐性开销。

数组与集合

forEach 或 map 遍历时修改原数组,哪些变化会被看到?

把链式调用合成一个循环后,回调交错执行,要确认回调里有没有读写原数组。

数组与集合

数组的 toString 和 join 有什么区别,嵌套数组真的被展平了吗?

批量拼接字符串的开销来自字符串分配,和数组容器的分配是两回事,可以对照着看。

参考资料

  • MDN:map
  • MDN:sort
  • MDN:Array

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 数一数到底创建了几个数组
  3. 合并成一个循环会改变什么
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑