JavaScript 数组扁平化最直接的写法是 Array.prototype.flat:指定层级用 flat(depth),未知层级可用 flat(Infinity)。需要兼容旧环境、同时转换元素或理解实现原理时,再考虑 concat、reduce 或递归。

接口返回了多层分类数据,你只想得到一维列表;或者表格批量选择结果是数组套数组,提交前必须展开。数组扁平化看似简单,但空位、层级、超深嵌套和非数组值都会改变结果。本文用四种可运行写法比较 JavaScript 数组扁平化的边界和性能取舍。今天这篇文章,编程狮带你从“会写”走到“知道为什么这样选”。
一、先确定数组要展开多少层
数组扁平化是把嵌套数组中的元素提升到更外层。开始写代码前要先确定目标:只展开一层、固定展开两三层,还是不确定层级并希望得到完全一维数组。
const source = [1, [2, [3, [4]]]];
console.log(source.flat()); // [1, 2, [3, [4]]]
console.log(source.flat(2)); // [1, 2, 3, [4]]
console.log(source.flat(Infinity)); // [1, 2, 3, 4]
flat 的默认 depth 是 1。很多错误不是方法不会用,而是没有把输出层级写进需求。如果你还不熟悉数组方法,可以先看 JavaScript 基础教程,再回到本文比较方案。
⚠️ 注意:flat 会跳过数组中的空位。空位与值为 undefined 的元素不是同一回事,依赖索引位置时必须先测试数据。
二、写法一:用 flat 处理固定或未知层级
现代环境中,flat 是首选。它表达清楚,不需要维护递归状态。固定层级时传入数字,确认要完全展开时传入 Infinity。
const categoryIds = [[101, 102], [201, [202, 203]]];
const twoLevels = categoryIds.flat(2);
console.log(twoLevels); // [101, 102, 201, 202, 203]
不要习惯性使用 Infinity。如果数据层级本应最多为 2,固定 depth 能保留异常结构,让你更早发现接口返回错误。Infinity 更适合树深度确实不固定,并且叶子值才是最终目标的场景。
flat 只展开真正的数组,不会把普通可迭代对象或字符串拆开。这个边界通常是好事,因为它不会意外改变业务对象。
三、写法二:flatMap 同时转换并展开一层
flatMap 等价于先 map,再 flat(1),适合“每个输入生成零个或多个输出”的场景。例如把课程分组转换成带分组名的课程列表,或者在转换时过滤无效项。
const groups = [
{ name: "前端", lessons: ["HTML", "CSS"] },
{ name: "编程", lessons: ["JavaScript"] }
];
const lessons = groups.flatMap(group =>
group.lessons.map(title => ({ group: group.name, title }))
);
console.log(lessons);
这段代码只展开 map 返回结果的一层。如果 lessons 自己还有多层嵌套,flatMap 不会递归处理。它的优势是意图明确:转换和一层展开属于同一个业务动作。
需要核对 flat、flatMap 或 reduce 的参数时,JavaScript 速查手册 可以直接查方法签名。
四、写法三:reduce 配合 concat 兼容旧环境
旧环境没有 flat 时,可以用 reduce 累加结果,再用 concat 展开一层。这个写法适合层级固定、兼容目标明确的项目。
function flattenOneLevel(list) {
return list.reduce((result, item) => result.concat(item), []);
}
const source = [1, [2, 3], 4, [5]];
console.log(flattenOneLevel(source)); // [1, 2, 3, 4, 5]
concat 遇到数组会展开一层,遇到普通值则直接追加。它不会自动处理更深层级。需要多层时可以重复调用,但重复次数应和业务层级一致,不要通过 while 循环盲目展开未知数据。
这种方案的可读性不如 flat,但在需要兼容旧浏览器且不方便加入 polyfill 时很实用。项目已有构建工具时,通常更适合使用标准写法并交给兼容配置处理。
五、写法四:递归或迭代处理任意深度
手写递归能加入过滤、路径记录和最大深度限制。下面的实现只展开数组,其他值原样保留:
function flattenDeep(input) {
const result = [];
function visit(items) {
for (const item of items) {
if (Array.isArray(item)) {
visit(item);
} else {
result.push(item);
}
}
}
visit(input);
return result;
}
console.log(flattenDeep([1, [2, [3, [4]]]]));
递归写法容易理解,但极端深度可能触发调用栈限制。数据深度来自用户输入或不可信来源时,可以用显式栈改写:
function flattenWithStack(input) {
const stack = [...input].reverse();
const result = [];
while (stack.length > 0) {
const item = stack.pop();
if (Array.isArray(item)) {
for (let i = item.length - 1; i >= 0; i -= 1) {
stack.push(item[i]);
}
} else {
result.push(item);
}
}
return result;
}
显式栈避免函数层层调用,并通过逆序压栈保持原始元素顺序。它的代码更长,只有在深度不可控或需要遍历过程信息时才值得使用。
5.1 4 种数组扁平化写法怎么选
| 写法 | 展开层级 | 适合场景 | 主要注意点 |
|---|---|---|---|
| flat | 固定或全部 | 现代环境的一般场景 | 会跳过空位 |
| flatMap | 一层 | 转换后产生多个结果 | 只能展开一层 |
| reduce + concat | 一层 | 旧环境兼容 | 可读性略低 |
| 递归或栈 | 任意 | 自定义规则、深度不可控 | 需要自己测试边界 |
不要只用小数组做性能结论。真实选择通常先看可读性和兼容性,再看数据规模。对于几十或几百个元素,标准方法带来的清晰度往往比微小耗时差异更重要;对于大数据,应使用实际样本测量内存和耗时。
还要注意去重不是数组扁平化的一部分。展开后的重复元素会被保留。如果业务需要唯一值,应在扁平化完成后再根据数据类型选择 Set、Map 或键函数处理。关于这一点,可以参考 JavaScript 数组去重笔记。

总结
JavaScript 数组扁平化先看目标层级,再选实现。现代环境优先 flat;转换并展开一层用 flatMap;旧环境可以用 reduce 配合 concat;需要自定义规则或应对未知深度时,再选择递归或显式栈。
你需要带走三点:depth 应反映业务结构;空位和 undefined 要分开测试;扁平化、过滤和去重是不同动作。下一步可以把项目中的嵌套数组样本加入测试,确保方法升级后输出顺序和类型保持一致。
延伸学习
想系统掌握数组与工程写法,可以按这个顺序继续:
- 先学 JavaScript 原生模块课程,把工具函数组织成可复用模块;
- 再读 JavaScript 判断空数组,补齐数组边界判断;
- 最后用 JavaScript 实战项目 把扁平化、筛选和渲染串成完整练习。
常见问题
Q:flat(Infinity) 会很慢吗?
不一定。耗时取决于数组规模、深度和运行时。普通业务数据通常无需担心;数据非常大或深度不可控时,应使用真实样本测量,并考虑显式栈避免调用栈问题。
Q:flat 会修改原数组吗?
不会。flat 返回一个新数组,原数组结构保持不变。但数组里的对象元素仍是原对象引用,修改这些对象属性仍可能影响原数据。
Q:flatMap 能完全替代 map 加 flat 吗?
只能替代 map 后再展开一层的情况。需要展开两层以上时,仍要继续调用 flat 或换用其他方案。flatMap 更适合一个输入产生零个或多个输出的表达。
Q:为什么数组空位展开后消失了?
这是 flat 的标准行为。它会移除被处理层级中的空位,但不会移除显式写入的 undefined。依赖索引位置时,应先把空位转换成明确值或避免使用 flat。

免费 AI IDE



