先记住这个答案
普通数组 map 和 filter 分别创建新的结果数组,sort 则修改并返回调用它的数组,不是三步都创建一份新容器。链式处理还可能在回调中创建对象或字符串,这些分配与外层数组数量不同。若筛选和映射都是纯函数且语义允许,可以用一个循环筛选并写入最终结果,减少中间数组;但会改变回调交错顺序,稀疏输入、第三参数和副作用也可能导致行为不等价。应先验证结果与执行契约,再在真实热点测量内存和时间,避免只为少一个数组牺牲清晰的业务步骤。
- map 与 filter 新建数组,sort 原地修改
- 容器、元素对象和字符串分配要分别计算
- 融合循环前检查回调顺序与输入结构
用身份断言纠正所有方法都分配的误解
示例先 map,再 filter,最后 sort,分别保存引用以观察关系。排序返回值等于过滤结果,说明它直接修改了该数组;映射和过滤结果则是不同容器。
再用显式循环完成相同的纯数值变换和筛选,最后仍排序。该例只针对密集数字数组,条件依赖变换后的数值,因此循环保持这个顺序;不能把谓词随意移到映射之前。
const source = [3, -1, 2];
const mapped = source.map(value => value * 2);
const filtered = mapped.filter(value => value > 0);
const sorted = filtered.sort((a, b) => a - b);
console.log(mapped === source, filtered === mapped, sorted === filtered);
const fused = [];
for (const value of source) {
const doubled = value * 2;
if (doubled > 0) fused.push(doubled);
}
fused.sort((a, b) => a - b);
console.log(JSON.stringify(sorted));
console.log(JSON.stringify(fused));查看输出与解释
false false true
[4,6]
[4,6]链式版本生成映射与过滤容器,sort 在过滤容器上工作。融合版本少一个完整中间数组,但这个结果对照不是性能基准,也不证明含副作用的回调可以无条件同样改写。
回调执行顺序是等价性的重要部分
先 map 再 filter 会先完成所有映射,再开始筛选;融合循环则每项映射后马上筛选。若回调写日志、读共享状态或可能抛错,执行顺序和已经完成的副作用都可能不同。
map 与 filter 的回调还会收到各自的数组参数,融合后未必存在同样的完整中间数组。稀疏数组的跳过行为也与 for...of 不同,所以优化前应明确输入为密集受控数据还是需要保留属性语义。
峰值内存取决于引用存活而非只有方法数量
如果映射结果仍被其他变量或闭包持有,它不会因为下一步过滤完成就立即消失。每项还创建大对象时,元素体积可能比数组槽位更重要,应查看引用链、分配热点和峰值负载。
对于小列表,清楚的 map/filter 往往比手工融合更容易维护。只有在确认热点后才减少中间分配,并保留代表性结果校验;无须把所有表达式都改成复杂 reduce 来追求表面上的单次遍历。
容易答错的地方
- 说 sort 也一定生成新数组
- 普通 sort 返回原数组并修改它,混淆这一点既会算错分配,也会漏掉共享引用副作用。若使用 toSorted 等复制方法,应按那个具体 API 重新分析,不要把名称相近的方法合并概括。
- 把多个回调融合后只检查最终数字
- 相同输出不代表回调顺序、异常位置和副作用完全相同。应先确认函数纯度、输入结构和第三参数使用,再决定是否融合,避免性能修改改变业务执行过程。
面试官还会怎么问?
用 reduce 就不会创建中间对象了吗?
不会自动消除,回调里若每步展开累加器或构造对象,仍可能产生大量分配。应分析实际代码的创建与复制行为,方法名不能代表内存复杂度或是否只遍历一次。
filter 放在 map 前面一定更省吗?
只有谓词能直接在原始输入上等价判断时才可能这样重排。若条件依赖映射结果,提前筛选可能改变语义;即使可以重排,也应考虑回调成本与错误处理顺序。
怎样判断优化是否值得保留?
在代表性数据规模和目标运行环境上比较总耗时、峰值内存与延迟,并确认语义测试通过。若收益很小而实现明显更复杂,应优先可读性,避免长期维护一个没有实际收益的特殊路径。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。