本文详解JavaScript数组扁平化技术,涵盖原生Array.prototype.flat()的depth参数与空位处理机制。深入剖析通过递归与栈结构手动实现flat方法的多种方案,对比for、forEach、reduce等遍历方式及数组判断技巧,提供面试常用的高阶实现代码。
1 数组扁平化的定义
该概念的核心在于将多维数组结构转换为低维或一维结构,例如:
// 原数组是一个“三维”数组
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
// 可以降成二维
newArray1 = [1, 2, 3, 4, [5, 6], 7, 8, 9]
// 也可以降成一维
newArray2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
这一过程在技术上也常被称为数组扁平化或数组降维。
2 JavaScript标准库中的flat()方法
JavaScript原生标准库已内置数组扁平化功能,即Array.prototype.flat()。
该方法依据指定的深度参数进行递归遍历,将当前数组及其子数组中的所有元素合并,最终返回一个全新的扁平化数组。
语法格式为:var newArray = arr.flat([depth])
参数depth为可选值,用于指定递归展开的深度层级,默认值为1。它决定了数组需要被降维的层数。
返回值是一个包含原数组及所有子数组元素的新数组。
具体示例如下:
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
const newArray1 = array.flat() // 等价于array.flat(1);降1维
// newArray1: [1, 2, 3, 4, [ 5, 6 ], 7, 8, 9]
const newArray2 = array.flat(2) // 降2维
// newArray2:[1, 2, 3, 4, 5, 6, 7, 8, 9]
特殊行为说明:
当depth小于等于0时,返回的数组维数与原数组保持一致(需注意,这仅指维度结构相同,空位处理情况请参考第3点说明)。
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
array.flat(-1)
// [1, 2, [3, 4, [5, 6], 7], 8, 9]
当depth设置为Infinity时,无论原数组多少维,最终结果都将完全展开为一维数组。
array.flat(Infinity)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
若原数组包含空位,flat方法会将其消除,即便调用flat(0)也会消除空位,因此第1点仅强调“维数一样”。此外,flat方法仅消除已展开层级内的空位,深层未展开层级的空位将保留。
const array1 = [1, , 2, [3, ,4, [5, 6], 7], 8, 9]
// 注意这个数组有两个空位
array.flat(0)
// [ 1, 2, [ 3, ,4, [ 5, 6 ], 7 ], 8, 9 ]
// 第一个空位没了,第二个空位还在
3 手动实现flat方法
实现单层展开(降1维)的逻辑为:遍历数组,若当前元素非数组则直接保存;若为数组,则将其展开后保存。
实现多层展开(降多维)则是在单层展开的基础上,通过递归对子数组执行相同操作。
可将实现过程分解为以下三个步骤:
1、数组遍历方式的选择
2、数组类型的判断逻辑
3、递归函数的构建
组合上述三步即可得到多种flat方法的实现方案。
3.1 数组遍历方法
JavaScript提供了多种遍历方式,主要可分为三类:
1、for循环相关语法
- 传统的for循环
- for...of循环
for...in主要用于遍历对象属性,不建议直接用于数组遍历。
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
// for循环
for (let i = 0; i < array.length; i++) {
const element = array[i];
}
// for...of
for (const element of array) {
}
2、数组内置方法:可直接获取元素的方法
- forEach()
- reduce()
- map()
// forEach()
array.forEach(element => {
});
// reduce()
array.reduce((pre, cur) => {
const element = cur
}, [])
// map()
array.map(element => {
})
3、数组内置方法:返回遍历器(Iterator)对象的方法
- keys()
- values()
- entries()
// 这三种方式仅仅是获得遍历器对象,还需搭配for...of来进行遍历
// keys()
for (let i of array.keys()) {
const element = array[i]
}
// values()
for (let element of array.values() ) {
}
// entries()
for (let [i, element] of array.entries()) {
console.log(array[i])
console.log(element)
}
3.2 数组类型判断
假设变量为a,判断其是否为数组,有以下四种常用方法:
- Array.isArray()是Array的静态方法,专门用于检测变量是否为数组。
- instanceof运算符通过检测构造函数的prototype属性是否出现在实例对象的原型链上来判断。若a是数组,其原型链上必然包含Array.prototype。
- 通过对象的constructor属性判断(注意:此方法可能失效,因为constructor属性可被手动修改)。
- 通过Object.prototype.toString()判断,该方法返回表示对象类型的字符串。
// 方法1
Array.isArray(a)
// 方法2
a instanceof Array
// 方法3
a.constructor === Array
// 方法4
// 使用call来调用Object.prototype上的toString方法
Object.prototype.toString.call(a) === '[object Array]'
// 不能这么判断,因为这个toString已经覆盖了Object.prototype.toString
// 只有Object.prototype.toString能正确判断类型
a.toString()
3.3 递归逻辑
递归的核心在于对子元素执行与父元素相同的处理操作。
function flat() {
let res = []
遍历数组 {
if (当前元素是数组) {
flat(当前元素)得到一维数组
将一维数组拼接到res中
} else {
res.push(当前元素)
}
}
return res
}
3.4 初步实现flat方法
选择合适的遍历方式、数组判断方式并配合递归,即可初步实现flat方法,例如:
function myFlat(arr) {
let res = [];
for (const item of arr) {
if (Array.isArray(item)) {
res = res.concat(myFlat(item));
// 注意concat方法返回一个新数组,不会改变原数组
} else {
res.push(item);
}
}
return res;
}
上述myFlat方法可将“多维”数组完全拉平为一维,但存在两个局限:无法指定展开深度depth,且无法正确处理数组空位。
4 方法优化
4.1 支持指定展开深度
处理展开深度的逻辑较为简单,只需增加递归终止条件depth<=0,代码如下:
function myFlat(arr, depth = 1) {
// 若depth<=0,则直接返回
if (depth <= 0) {
return arr
}
let res = [];
for (const item of arr) {
if (Array.isArray(item)) {
// 每次递归调用,将depth-1
res = res.concat(myFlat(item, depth - 1));
} else {
res.push(item);
}
}
return res;
}
4.2 数组空位处理
在实际开发中,应尽量避免出现数组空位的情况。
前文提到的不同遍历方法对数组空位的处理机制存在差异。
forEach、reduce和map在遍历时遇到空位会直接忽略;而for...of不会忽略,会将空位视为undefined处理。
4.2.1 改进for...of遍历
因此,我们需要对基于for...of遍历的myFlat方法进行改进:
function myFlat(arr, depth = 1) {
if (depth <= 0) {
return arr;
}
let res = [];
for (const item of arr) {
if (Array.isArray(item)) {
res = res.concat(myFlat(item, depth - 1));
} else {
// 判断数组空位
item !== undefined && res.push(item);
}
}
return res;
}
4.2.2 使用forEach或map遍历
当然,也可以改用forEach或map方法遍历数组,从而免去手动判断空位的步骤。
但需注意一个特殊情况:当depth <= 0时,需使用filter方法来消除数组空位。
// forEach
function myFlat(arr, depth = 1) {
if (depth <= 0) {
return arr.filter(item => item !== undefined);
}
let res = [];
arr.forEach((item) => {
if (Array.isArray(item)) {
res = res.concat(myFlat(item, depth - 1));
} else {
res.push(item);
}
});
return res;
}
// map
function myFlat(arr, depth = 1) {
if (depth <= 0) {
return arr.filter(item => item !== undefined);
}
let res = [];
arr.map((item) => {
if (Array.isArray(item)) {
res = res.concat(myFlat(item, depth - 1));
} else {
res.push(item);
}
});
return res;
}
4.2.3 使用reduce方法
其中,使用reduce方法实现的代码最为简洁,也是面试中的高频考点之一。
function myFlat(arr, depth = 1) {
return depth > 0
? arr.reduce(
(pre, cur) =>
pre.concat(Array.isArray(cur) ? myFlat(cur, depth - 1) : cur),
[]
)
: arr.filter((item) => item !== undefined);
}
5 其他实现思路
5.1 栈结构实现
理论上,递归方法通常可以转换为非递归形式,即使用栈结构来实现。
function myFlat(arr) {
let res = [];
const stack = [].concat(arr);
while (stack.length > 0) {
const item = stack.pop();
if (Array.isArray(item)) {
// 用扩展运算符展开一层
stack.push(...item);
} else {
item !== undefined && res.unshift(item);
}
}
return res;
}
然而,此方法无法指定展开深度,只能将数组彻底展开为一维。
5.2 栈方法的改进
针对栈方法无法指定展开深度的缺点,可进行如下改进:
function myFlat(arr, depth = 1) {
if (depth <= 0) {
return arr.filter((item) => item !== undefined);
}
let res;
let queue = [].concat(arr);
while (depth > 0) {
res = [];
queue.forEach((item) => {
if (Array.isArray(item)) {
// 注意用扩展运算符将数组展开前先用filter方法去掉空位
res.push(...item.filter((e) => e !== undefined));
} else {
res.push(item);
}
});
depth--;
queue = res;
}
return res;
}
总结
本文介绍了JavaScript数组扁平化的多种实现方式,包括原生API、递归及栈结构。更多关于JS实现数组扁平化的内容,请查阅本站相关文章或继续浏览后续内容,感谢支持!