先记住这个答案
flatMap 把每项映射结果再展平一层,回调返回空数组可删除该项,返回单元素数组可替换,返回多个元素则可扩展。map 通常保持一项对应一项,filter 只决定保留或删除,因此遇到分词、拆分区间或一条记录生成多条展示项时,flatMap 更贴近结果模型。它不递归展平所有层级,返回普通非数组值也会直接作为一项保留。相比多段链式处理是否更快取决于临时数组分配、数据规模与引擎,不能把更短代码当作性能证明。
- 回调可以为一条输入生成零到多条输出
- 仅把回调结果中的数组展开一层
- 语义更直接不代表运行成本永远更低
分词场景自然需要一对多映射
例如三行标签输入中有一行只包含空格,有一行包含两个标签。业务需要的是最终标签列表,回调先清理当前行,再返回零个或多个词,就可以直接表达这条输入产生的结果。
若使用 map 加 filter,map 返回的词数组仍是嵌套数组,filter 只能删掉空行对应项,不能自动展开词。此时还需要独立展开步骤,flatMap 则把这个输出模型写在一次转换中。
const lines = [' red blue ', ' ', 'green'];
const words = lines.flatMap(line => {
const text = line.trim();
return text === '' ? [] : text.split(/\s+/);
});
console.log(JSON.stringify(words));
console.log(JSON.stringify([1].flatMap(value => [[value, value + 1]])));查看输出与解释
["red","blue","green"]
[[1,2]]空行生成空数组而被省去,第一行生成两个词。第二个例子仍保留内部数组,说明 flatMap 只展开回调结果的一层,不会把所有嵌套递归拍平。
明确什么时候不需要 flatMap
如果只是把每项数字乘二,map 已经准确表达一对一转换;如果只保留符合条件的原记录,filter 更清楚。不要为了少写一个方法名,让回调到处创建单元素数组却没有实际的一对多需求。
当筛选条件与转换互相依赖时,可以将两者放在同一回调,但仍应保持分支清楚。业务上无效记录是否删除、保留错误信息或终止整个处理,需要明确决定,不能一律返回空数组让问题悄悄消失。
检查分配、空位和对象身份
flatMap 仍会构造结果数组,回调返回的小数组也可能带来分配成本。热路径可与直接循环比较,同时测量峰值内存和实际耗时;在没有测量之前只应陈述语义优势。
对普通稀疏数组,回调通常不访问缺失索引,返回数组里的空位在展平时也可能被跳过。结果里的对象仍是原引用,后续修改可能影响源数据;变换容器结构不等于深复制业务对象。
容易答错的地方
- 声称 flatMap 永远比 map 加 filter 快
- 不同数据规模、回调成本和临时数组分配会影响结果,某些直接循环更省分配。应先选择清楚的语义,在真实热点和目标引擎上比较,不能用 API 数量替代性能证据。
- 返回对象里的数组就期待自动展开
- flatMap 只识别回调返回的数组这一层,不会递归进入任意对象属性。若业务需要展开 record.items,应明确返回该数组或转换后的结果,并处理缺失字段与允许的输入类型。
面试官还会怎么问?
回调返回 undefined 会把这一项删除吗?
不会,undefined 是普通返回值,会作为结果中的一个元素保留。需要删除应明确返回空数组,这也能让维护者看出该分支有意不产生输出,而不是遗漏 return。
flatMap 可以直接处理异步分词函数吗?
async 回调返回 Promise,flatMap 不会自动等待它,也不会把未来得到的数组立即展开。应先明确异步任务的收集、等待与并发策略,再对完成结果执行所需展开。
为什么拆成多个步骤有时更好?
筛选、校验和转换若承担不同业务责任,分开命名有助于调试和复用。flatMap 适合局部清晰的一对多模型,不要求把复杂管道塞进一个回调;可读性和错误处理仍是选择依据。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。