1.前言
我们为什么需要知道数组实现方法中的原理?拿来会用只是基础,当有问题出现的时候,如果你知道原理,那么问题就容易定位。比如接下来要谈的是数字的扁平化方法,在ES2019 中增加了数组的扁平化方法Array.prototype.flat
。如果你手动去实现这个方法,可以学习到数组的处理逻辑 、递归与迭代的区别以及它们的实际用法 、数据结构的实际应用
2.详解
数组的扁平化通俗来说,是将嵌套的数组结构从多维降到某维。 Array.prototype.flat
具备一个可选参数是depth
,默认值是 1,可以灵活指定需要扁平的层级,返回一个新的数组,对原数组进行浅拷贝。
ini
//depth默认是 1
const arr1 = [1, 2, [3, 4, [5, 6]]];
arr1.flat();
// [1, 2, 3, 4, [5, 6]]
const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat(2);
// [1, 2, 3, 4, 5, 6]
还有手动实现数组扁平化:
- ES5实现扁平化:for 循环(forEach遍历)+ 递归
- ES6+实现扁平化:reduce版本 + 递归
- ES6+实现扁平化:some 方法 + 扩展运算符
- ES6+实现扁平化:迭代 + 数据结构栈
3.适用场景
Array.prototype.flat
日常开发中的首选方案,但是也有一些特别情况不适用:
- 兼容旧老浏览器,可以使用 ES5 实现或者进行转译
- 在扁平化的基础上定制处理特殊的逻辑,可以使用 reduce 版本或者
Array.prototype.flatMap
方法 - 处理更深层次的数据,避免递归栈溢出,可以使用迭代版本栈结构的方式
4.优缺点
Array.prototype.flat
- 优点:简洁性能效率性能高,远超手动实现,推荐使用
- 缺点:兼容性不佳、无法处理定制逻辑、更深层次数据可能递归栈溢出
- for 循环(forEach遍历)+ 递归
- 优点:ES5实现兼容性好
- 缺点:性能不如原生实现的高
- reduce版本
- 优点: 可以处理定制逻辑,过滤一些条件
- 缺点: 性能不如原生实现的高
- 迭代 + 数据结构栈
- 优点:处理更深层次的数据,避免递归栈溢出
- 缺点: 性能不如原生实现的高
5.代码实现
5.1 ES5实现方式 forEach遍历 + 递归
- 声明一个
function
,参数一:需要扁平化的数组,参数二:支持的扁平化的深度depth
javascript
function flatten(arr,depth ) {}
- 对参数的类型进行判断,参数一是数组、参数二 depth 是非负整数,
depth
默认值是 1
javascript
funxction flatten (arr,depth) {
depth = depth === undefined?1:depth
if(!Array.is(arr)) {
throw new TypeError("第一个参数必须是数组类型")
} else if(typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {
throw new TypeError("第二个参数必须是非负整数")
}
//如果 depth 是 0,则返回对原数组进行浅拷贝的新数组
if(depth === 0) return arr.slice()
}
- 对数组子元素进行判断如果为数组,那么进行递归处理,不是数组就进入新数组中保存起来
scss
function flatten(arr,depth){
...
var reault = []
function flated(element){
if(Array.isArray(item)) {
element.forEach(function(item){
flated(item)
})
} else {
reault.push(item)
}
}
flated(arr)
return result
}
- 支持灵活设置深度,既要每个递归层级深度逐渐递增,也要保留每一层级的当前 depth 值,所以需要注意递增深度值使用
curDepth + 1
,而不是curDepth++
scss
function flatten(arr,depth){
...
var reault = []
function flated(element,curDepth){
if(curDepth > depth){
result.push(element)
return result
}
if(Array.isArray(element)) {
element.forEach(item => {
flated(item, curDepth + 1)
})
} else {
reault.push(element)
}
}
flated(arr,0)
return result
}
5.全部实现代码
javascript
function flatten (arr,depth) {
depth = depth === undefined?1:depth
if(!Array.isArray(arr)) {
throw new TypeError('第一个参数必须是数组');
} else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {
throw new TypeError("第二个参数必须是非负整数")
}
//如果 depth 是 0,则返回对原数组进行浅拷贝的新数组
if(depth === 0) return arr.slice()
const result = []
function flated (element,curDepth) {
if(curDepth > depth) {
result.push(element)
return result
}
if(Array.isArray(element)) {
element.forEach(item => {
flated(item, curDepth + 1)
})
} else {
arr.push(element)
}
}
flated(arr,0)
return result
}
module.exports = flatten;
5.2 reduce实现扁平化版本
- 核心是会使用reduce 方法,数组的reduce 方法,具备两个参数,
- 参数一回调函数
callbackFn
:为每个数组元素执行的函数,其返回值是下一次执行回调函数的中的accumulator
参数的值,如果是最后一次调用,那么就会作为reduce()
方法的返回值。callbackFn
具备三个参数accumulator
:上一次调用callbackFn
的结果。在第一次调用时,如果指定了initialarr
则为指定的值,否则为array[0]
的值。currentarr
: 当前元素的值。在第一次调用时,如果指定了initialarr
,则为array[0]
的值,否则为array[1]
。currentIndex
:currentarr
在数组中的索引位置。在第一次调用时,如果指定了initialarr
则为 0,否则为 1。
- 参数二是初始值
initialarr
,第一次调用回调函数callbackFn
时初始化accumulator
的值,是个可选参数,如果传了,则回调函数callbackFn
从第一个元素开始执行,如果不传,那么初始值就默认为第一个元素,回调函数callbackFn
从第二个元素开始执行。
javascript
//核心代码,其他逻辑与上面5.1一致
const flated = (element,curDepth) => {
if(curDepth >= depth) return element
return element.reduce((acl,curVal) => {
if(Array.isArray(curVal)) {
return acl.concat(flated(curVal,curDepth + 1))
} else {
return acl.concat(curVal)
}
},[])
}
- 实现完整的代码
javascript
const flatten = (arr,depth = 1) => {
if(!Array.isArray(arr)) {
throw new TypeError('第一个参数必须是数组');
} else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {
throw new TypeError("第二个参数必须是非负整数")
}
if(depth === 0) return arr.slice()
const flated = (element,curDepth) => {
if(curDepth >= depth) return element
return element.reduce((acl,curVal) => Array.isArray(curVal)?acl.concat(flated(curVal,curDepth + 1)):acl.concat(curVal),[])
}
return flated(arr, 0)
}
5.3 扩展运算符实现扁平化
- 核心是采用 some 方法,通过循环遍历判断是否是嵌套结构,如果是则继续展开
javascript
const flatten = (arr,depth = 1) => {
if(!Array.isArray(arr)) {
throw new TypeError('第一个参数必须是数组');
} else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {
throw new TypeError("第二个参数必须是非负整数")
}
if(depth === 0) return arr.slice()
let result = [...arr]
while(result.some(item => Array.isArray(item))) {
result = [].concat(...result)
}
return result
}
5.4 迭代 + 栈结构实现扁平化
- 核心点需要满足栈的特点,后进先出,直到栈中为空,则停止循环遍历
c
const faltten = (arr,depth) => {
...
//记录栈中的每个元素当前的深度,初始值为 0
const stack = arr.map(item => [item,0])
//记录最终结果
const result = []
while(stack.length) {
//先取出栈中最后进入的值和当前 depth
const [currentVal,curDepth] = stack.pop()
if(Array.isArray(currentVal) && curDepth < depth) {
stack.push(...currentVal.map(item => [item,curDepth + 1]))
} else {
result.unshift(currentVal)
}
}
return result
}
- 完整代码
javascript
const faltten = (arr,depth) => {
if(!Array.isArray(arr)) {
throw new TypeError('第一个参数必须是数组');
} else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {
throw new TypeError("第二个参数必须是非负整数")
}
if(depth === 0) return arr.slice()
//记录栈中的每个元素当前的深度,初始值为 0
const stack = arr.map(item => [item,0])
//记录最终结果
const result = []
while(stack.length) {
//先取出栈中最后进入的值和当前 depth
const [currentVal,curDepth] = stack.pop()
if(Array.isArray(currentVal) && curDepth < depth) {
stack.push(...currentVal.map(item => [item,curDepth + 1]))
} else {
result.unshift(currentVal)
}
}
return result
}
6.测试验证
单元测试,使用了Jest
框架,下面是测试的几个测试用例,完全通过
- 5.1/5.2/5.4版本,能够支持灵活设置 depth 的测试用例是相同的一套
scss
const flatten = require('../xxxx.js');
describe('数组扁平化函数测试', () => {
// 基础功能测试
test('不指定深度时默认展开1层', () => {
const arr = [1, [2, 3], [4, [5]]];
expect(flatten(arr)).toEqual([1, 2, 3, 4, [5]]);
});
test('指定深度为1时展开1层', () => {
const arr = [1, [2, [3]], 4];
expect(flatten(arr, 1)).toEqual([1, 2, [3], 4]);
});
test('指定深度为2时展开2层', () => {
const arr = [1, [2, [3, [4]]], 5];
expect(flatten(arr, 2)).toEqual([1, 2, 3, [4], 5]);
});
// 边界情况测试
test('深度为0时不展开任何层级', () => {
const arr = [1, [2, [3]]];
expect(flatten(arr, 0)).toEqual([1, [2, [3]]]);
});
test('深度大于实际嵌套层级时完全展开', () => {
const arr = [1, [2, [3]]];
expect(flatten(arr, 10)).toEqual([1, 2, 3]);
});
// 特殊数组测试
test('空数组返回空数组', () => {
expect(flatten([])).toEqual([]);
});
test('非嵌套数组不变化', () => {
const arr = [1, 2, 3];
expect(flatten(arr, 5)).toEqual([1, 2, 3]);
});
test('包含空数组的情况', () => {
const arr = [1, [], [2, [[]]], 3];
expect(flatten(arr, 2)).toEqual([1, 2, [], 3]);
});
// 包含不同类型元素的数组
test('包含多种数据类型的数组', () => {
const arr = [ 'a', [1, true],
[null, [undefined, { key: 'arr' }]]
];
expect(flatten(arr, 2)).toEqual([
'a', 1, true, null, undefined, { key: 'arr' }
]);
});
// 异常处理测试
test('非数组参数抛出错误', () => {
expect(() => flatten('not an array')).toThrow(TypeError);
});
test('非数字深度参数抛出错误', () => {
expect(() => flatten([1, 2], 'deep')).toThrow(TypeError);
});
test('负数深度参数抛出错误', () => {
expect(() => flatten([1, 2], -1)).toThrow(TypeError);
});
});
- 5.3扩展运算符实现版本,只能支持完全展开,不支持灵活设置深度,下面是它的测试用例
scss
const flatten = require('../xxx');
describe('数组扁平化函数测试', () => {
// 基础功能测试
test('深度大于实际嵌套层级时完全展开', () => {
const arr = [1, [2, [3]]];
expect(flatten(arr)).toEqual([1, 2, 3]);
});
// 特殊数组测试
test('空数组返回空数组', () => {
expect(flatten([])).toEqual([]);
});
test('非嵌套数组不变化', () => {
const arr = [1, 2, 3];
expect(flatten(arr)).toEqual([1, 2, 3]);
});
test('包含空数组的情况', () => {
const arr = [1, [], [2, [[]]], 3];
expect(flatten(arr)).toEqual([1, 2, 3]);
});
// 包含不同类型元素的数组
test('包含多种数据类型的数组', () => {
const arr = [ 'a', [1, true],
[null, [undefined, { key: 'value' }]]
];
expect(flatten(arr)).toEqual([
'a', 1, true, null, undefined, { key: 'value' }
]);
});
// 异常处理测试
test('非数组参数抛出错误', () => {
expect(() => flatten('not an array')).toThrow(TypeError);
});
});
7.总结
在日常开发中,还是推荐使用原生的Array.prototype.flat
方法,它是由JavaScript 引擎(如 V8)底层实现,经过高度优化,在大多数场景下还是优于手动实现的递归和迭代的方法。 但是在一些特殊的场景,我们也可以进行手动实现,比如对兼容性要求比较高,需要处理一些定制化的逻辑,以及数据层级很深,那么我们就可以根据实际情况去判断使用哪种方法更为合适。