JavaScript 数据扁平化方法大全
前言
数据扁平化是指将多维数组转换为一维数组的过程。由于嵌套数据结构增加了访问和操作数据的复杂度,所以·我们可以将嵌套数据变成一维的数据结构,下面就是我搜集到的一些方法,希望可以给你带来帮助!!
1. 使用 Array.prototype.flat()(推荐)
ES2019 引入的专门方法:
const nestedArr = [1, [2, [3, [4]], 5]];
// 默认只扁平化一层
const flattened1 = nestedArr.flat();
console.log(flattened1); // [1, 2, [3, [4]], 5]
// 指定深度为2
const flattened2 = nestedArr.flat(2);
console.log(flattened2); // [1, 2, 3, [4], 5]
// 完全扁平化
const fullyFlattened = nestedArr.flat(Infinity);
console.log(fullyFlattened); // [1, 2, 3, 4, 5]
解析:
-
flat(depth)
方法创建一个新数组,所有子数组元素递归地连接到指定深度 - 参数
depth
指定要提取嵌套数组的结构深度,可选的参数,默认为1 - 使用
Infinity
可展开任意深度的嵌套数组,Infinity
是一个特殊的数值,表示无穷大。
2. 使用 reduce() 和 concat() 递归
function flatten(arr) {
// 使用 reduce 方法遍历数组元素
return arr.reduce((acc, val) => {
// 如果当前元素是数组,则递归调用 flatten 继续展开,并拼接到累积数组 acc
if (Array.isArray(val)) {
return acc.concat(flatten(val));
}
// 如果当前元素不是数组,直接拼接到累积数组 acc
else {
return acc.concat(val);
}
}, []); // 初始累积值是一个空数组 []
}
// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]
解析:
-
递归处理嵌套数组
- 遇到子数组时,递归调用
flatten(val)
继续展开,直到所有层级都被展开为单层。
- 遇到子数组时,递归调用
-
reduce
方法的作用- 遍历数组,通过
acc
(累积值)逐步拼接结果,初始值设为[]
(空数组)。
- 遍历数组,通过
-
Array.isArray(val)
检查- 判断当前元素是否为数组,决定是否需要递归展开。
-
concat
拼接结果- 将非数组元素或递归展开后的子数组拼接到累积数组
acc
中。
- 将非数组元素或递归展开后的子数组拼接到累积数组
3. 使用 concat()
和扩展运算符递归
function flatten(arr) {
// 使用扩展运算符 (...) 展开数组的第一层,并合并成一个新数组
const flattened = [].concat(...arr);
// 检查当前展开后的数组中是否仍然包含嵌套数组
// 如果存在嵌套数组,则递归调用 flatten 继续展开
// 如果所有元素都是非数组类型,则直接返回展开后的数组
return flattened.some(item => Array.isArray(item))
? flatten(flattened)
: flattened;
}
// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]
解析:
-
[].concat(...arr)
展开一层数组- 使用扩展运算符
...
展开arr
的最外层,并通过concat
合并成一个新数组。 - 例如:
[].concat(...[1, [2, [3]]])
→[1, 2, [3]]
(仅展开一层)。
- 使用扩展运算符
-
flattened.some(Array.isArray)
检查嵌套- 使用 Array.prototype.some() 检查当前数组是否仍然包含子数组。
- 如果存在,则递归调用
flatten
继续展开。
-
递归终止条件
- 当
flattened
不再包含任何子数组时,递归结束,返回最终结果。
- 当
4. 使用 toString()
方法(仅适用于数字数组)
const nestedArr = [1, [2, [3, [4]], 5]];
const flattened = nestedArr.toString().split(',').map(Number);
console.log(flattened); // [1, 2, 3, 4, 5]
解析:
-
toString()
的隐式转换- JavaScript 的
Array.prototype.toString()
会自动展开嵌套数组,并用逗号连接所有元素。 - 例如:
[1, [2, [3]]].toString()
→"1,2,3"
。
- JavaScript 的
-
split(',')
分割字符串- 将字符串按逗号拆分成字符串数组,但所有元素会是字符串类型(如
"2"
)。
- 将字符串按逗号拆分成字符串数组,但所有元素会是字符串类型(如
-
map(Number)
类型转换- 通过
Number
构造函数将字符串元素转换为数字类型。 -
注意:如果原数组包含非数字(如
['a', [2]]
),结果会变成[NaN, 2]
。
- 通过
优缺点:
-
优点:代码极其简洁,适合纯数字的嵌套数组。
-
缺点:
- 仅适用于数字数组(其他类型会被强制转换,如
true
→1
,null
→0
)。 - 无法保留原数据类型(如字符串
'3'
会被转成数字3
)。
- 仅适用于数字数组(其他类型会被强制转换,如
适用场景:
- 快速展开纯数字的嵌套数组,且不关心中间过程的性能损耗(
toString
和split
会有临时字符串操作)。
5. 使用 JSON.stringify() 和正则表达式
function flatten(arr) {
// 1. 使用 JSON.stringify 将数组转换为字符串表示
// 例如:[1, [2, [3]], 'a'] → "[1,[2,[3]],\"a\"]"
const jsonString = JSON.stringify(arr);
// 2. 使用正则表达式移除所有的 '[' 和 ']' 字符
// 例如:"[1,[2,[3]],\"a\"]" → "1,2,3,\"a\""
const withoutBrackets = jsonString.replace(/[\[\]]/g, '');
// 3. 按逗号分割字符串,生成字符串数组
// 例如:"1,2,3,\"a\"" → ["1", "2", "3", "\"a\""]
const stringItems = withoutBrackets.split(',');
// 4. 尝试将每个字符串解析回原始数据类型
// - 数字会变成 Number 类型(如 "1" → 1)
// - 字符串会保留(如 "\"a\"" → "a")
// - 其他 JSON 可解析类型也会被正确处理
return stringItems.map(item => {
try {
// 尝试 JSON.parse 解析(处理字符串、数字等)
return JSON.parse(item);
} catch (e) {
// 如果解析失败(如空字符串或非法 JSON),返回原始字符串
return item;
}
});
}
// 测试用例
const nestedArr = [1, [2, [3, [4]], 5, 'a', { b: 6 }];
console.log(flatten(nestedArr));
// 输出: [1, 2, 3, 4, 5, "a", { b: 6 }]
解析:
-
JSON.stringify
的作用- 将整个数组(包括嵌套结构)转换为 JSON 字符串,保留所有数据类型信息。
-
正则替换
/[[]]/g
- 移除所有方括号字符
[
和]
,只保留逗号分隔的值。
- 移除所有方括号字符
-
split(',')
分割字符串- 生成一个字符串数组,但每个元素可能仍是被 JSON 字符串化的(如
""a""
)。
- 生成一个字符串数组,但每个元素可能仍是被 JSON 字符串化的(如
-
JSON.parse() 尝试恢复数据类型
- 通过
JSON.parse
将字符串转换回原始类型(数字、字符串、对象等)。 - 使用
try-catch
处理不合法的 JSON 字符串(如空字符串或格式错误的情况)。
- 通过
优缺点:
-
优点:
- 支持任意数据类型(数字、字符串、对象等)。
- 能正确处理嵌套对象(如
{ b: 6 }
)。
-
缺点:
- 性能较低(涉及 JSON 序列化、正则替换、解析等操作)。
- 如果原始数组包含特殊字符串(如
"[1]"
) ,可能会被错误解析。
适用场景:
- 需要处理混合数据类型(非纯数字)的嵌套数组。
- 对性能要求不高,但需要代码简洁的场景。
6. 使用堆栈的非递归实现
function flatten(arr) {
// 创建栈并初始化(使用扩展运算符浅拷贝原数组)
const stack = [...arr];
const result = [];
// 循环处理栈中的元素
while (stack.length) {
// 从栈顶取出一个元素
const next = stack.pop();
if (Array.isArray(next)) {
// 如果是数组,展开后压回栈中(保持顺序)
stack.push(...next);
} else {
// 非数组元素,添加到结果数组前端(保持原顺序)
result.unshift(next);
}
}
return result;
}
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]
解析:
-
栈结构初始化
- 使用扩展运算符
[...arr]
创建原数组的浅拷贝作为初始栈 - 避免直接修改原数组
- 使用扩展运算符
-
栈处理循环
- 使用
while
循环处理栈直到为空 - 每次从栈顶
pop()
一个元素进行处理
- 使用
-
元素类型判断
- 使用
Array.isArray()
检查元素是否为数组 - 如果是数组则展开后重新压入栈
- 非数组元素则添加到结果数组
- 使用
-
顺序保持
- 使用
unshift()
将元素添加到结果数组前端,当然这样比较费性能,可以改用push()
+reverse()
替代unshift()
- 确保最终结果的顺序与原数组一致
- 使用
优缺点
-
优点:
- 支持任意数据类型(不限于数字)
- 可以处理深层嵌套结构(无递归深度限制)
- 相比递归实现,不易导致栈溢出
-
缺点:
- 使用 unshift() 导致时间复杂度较高(O(n²))
- 需要额外空间存储栈结构
- 相比原生
flat()
方法性能稍差 - 无法控制扁平化深度(总是完全扁平化)
适用场景
- 需要处理混合数据类型的深层嵌套数组
- 需要避免递归导致的栈溢出风险
7. 使用 Array.prototype.some()
和扩展运算符
function flatten(arr) {
// 循环检测数组中是否还包含数组元素
while (arr.some(item => Array.isArray(item))) {
// 使用扩展运算符展开当前层级的所有数组
// 并通过concat合并为一层
arr = [].concat(...arr);
}
return arr;
}
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]
解析:
-
循环条件检测
- 使用
arr.some()
方法检测数组中是否还存在数组元素 -
Array.isArray(item)
判断每个元素是否为数组
- 使用
-
层级展开
- 使用扩展运算符
...arr
展开当前层级的数组 - 通过
[].concat()
将展开的元素合并为新数组
- 使用扩展运算符
-
迭代处理
- 每次循环处理一层嵌套
- 重复直到没有数组元素存在
性能比较
对于大多数现代应用:
- 优先使用
flat(Infinity)
(最简洁且性能良好) - 对于深度嵌套的大数组,考虑非递归的堆栈实现
- 递归方法在小数据集上表现良好且代码简洁
- 避免
toString()
方法除非确定只有数字数据
总结
JavaScript 提供了多种扁平化数组的方法,从简单的内置 flat()
方法到各种手动实现的递归、迭代方案。选择哪种方法取决于:
- 运行环境是否支持 ES2019+
- 数据结构的复杂程度
- 对性能的要求
- 代码可读性需求
在大多数现代应用中,flat(Infinity)
是最佳选择,因为它简洁、高效且语义明确。