本文深入解析JavaScript数组扁平化的多种实现方式,涵盖递归、toString转换、reduce累加器及ES6扩展运算符。重点剖析Underscore库中flatten函数的源码逻辑,详解strict与shallow参数的配置对处理非数组元素及扁平层级的影响,并探讨其在union、difference等API中的应用。
数组扁平化概述
扁平化
数组扁平化的核心目标,是将一个可能包含任意层级嵌套的数组结构,转换为一个仅包含单一层级的数组。
假设存在一个名为 flatten 的函数,其执行效果应如下所示:
var arr = [1, [2, [3, 4]]];
console.log(flatten(arr)) // [1, 2, 3, 4]
明确了预期输出结果后,接下来可以着手编写实现该功能的 flatten 函数。
基础实现方案
递归
最直观的思路是遍历数组元素,若检测到元素仍为数组类型,则递归调用当前函数进行处理:
// 方法 1
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
}
else {
result.push(arr[i])
}
}
return result;
}
console.log(flatten(arr))
toString
若数组内仅包含数字元素,可利用 toString 方法,因为:
[1, [2, [3, 4]]].toString() // "1,2,3,4"
调用 toString 后会生成一个以逗号分隔的扁平字符串,此时再使用 split 分割并转换为数字类型,即可实现扁平化。
// 方法2
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.toString().split(',').map(function(item){
return +item
})
}
console.log(flatten(arr))
然而,该方法的适用场景极为有限。例如当数组包含混合类型如 [1, '1', 2, '2'] 时,此方法将返回错误结果。
reduce
鉴于处理数组并最终返回单一值的需求,可以考虑使用 reduce 方法来简化代码逻辑:
// 方法3
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
console.log(flatten(arr))
ES6 引入了扩展运算符,用于提取参数对象中所有可遍历的属性,并将其拷贝至当前对象中:
var arr = [1, [2, [3, 4]]];
console.log([].concat(...arr)); // [1, 2, [3, 4]]
利用该方法仅能实现单层扁平化,但基于此思路进一步推导,可构建如下实现:
// 方法4
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr))
Underscore源码深度解析
undercore
如何设计一个抽象的扁平化函数以提升开发效率?这便需要参考 underscore 库的实现。
此处直接展示源码及注释,需注意此处的 flatten 函数并非最终的 _.flatten 版本。为了支持多个 API 的调用,该函数提供了更丰富的配置选项。
/**
* 数组扁平化
* @param {Array} input 要处理的数组
* @param {boolean} shallow 是否只扁平一层
* @param {boolean} strict 是否严格处理元素,下面有解释
* @param {Array} output 这是为了方便递归而传递的参数
* 源码地址:https://github.com/jashkenas/underscore/blob/master/underscore.js#L528
*/
function flatten(input, shallow, strict, output) {
// 递归使用的时候会用到output
output = output || [];
var idx = output.length;
for (var i = 0, len = input.length; i < len; i++) {
var value = input[i];
// 如果是数组,就进行处理
if (Array.isArray(value)) {
// 如果是只扁平一层,遍历该数组,依此填入 output
if (shallow) {
var j = 0, len = value.length;
while (j < len) output[idx++] = value[j++];
}
// 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output
else {
flatten(value, shallow, strict, output);
idx = output.length;
}
}
// 不是数组,根据 strict 的值判断是跳过不处理还是放入 output
else if (!strict){
output[idx++] = value;
}
}
return output;
}
关于 strict 参数的解释:在代码遍历过程中,若元素非数组类型,则依据 strict 取反后的值进行判断。若将 strict 设为 true,则跳过该元素不作处理,这意味着可以过滤掉非数组元素。例如:
var arr = [1, 2, [3, 4]];
console.log(flatten(arr, true, true)); // [3, 4]
那么设置 strict 参数的具体作用是什么?先观察 shallow 和 strict 不同组合下的结果:
- shallow true + strict false :正常扁平一层
- shallow false + strict false :正常扁平所有层
- shallow true + strict true :去掉非数组元素
- shallow false + strict true : 返回一个[]
接下来查看 underscore 中哪些方法调用了这个基础的 flatten 函数:
_.flatten
首先是 _.flatten 的实现:
_.flatten = function(array, shallow) {
return flatten(array, shallow, false);
};
在常规扁平化操作中,我们通常不需要过滤非数组元素。
_.union
接下来是 _.union 方法:
该函数接收多个数组作为参数,并返回这些数组的并集。
例如:
_.union([1, 2, 3], [101, 2, 1, 10], [2, 1]);
=> [1, 2, 3, 101, 10]
若传入的参数并非数组类型,则该参数将被跳过。
_.union([1, 2, 3], [101, 2, 1, 10], 4, 5);
=> [1, 2, 3, 101, 10]
为实现此效果,可将所有传入数组进行扁平化处理后再去重。由于该场景仅接受数组输入,直接设置 strict 为 true 即可跳过非数组元素。
// 关于 unique 可以查看《JavaScript专题之数组去重》[](https://github.com/mqyqingfeng/Blog/issues/27)
function unique(array) {
return Array.from(new Set(array));
}
_.union = function() {
return unique(flatten(arguments, true, true));
}
_.difference
看来 strict 参数确实发挥了作用,再来看 _.difference 方法:
其语法结构为:
_.difference(array, *others)
该函数的作用是从 array 数组中提取出不存在于其他多个 other 数组中的元素。与 _.union 类似,它同样会排除非数组类型的元素。
例如:
_.difference([1, 2, 3, 4, 5], [5, 2, 10], [4], 3);
=> [1, 3]
实现方式较为简单:先对 others 数组进行扁平化处理,再筛选出 array 中不在扁平化数组中的值:
function difference(array, ...rest) {
rest = flatten(rest, true, true);
return array.filter(function(item){
return rest.indexOf(item) === -1;
})
}
注意,上述实现细节并非完全复刻 underscore 源码,若对具体实现感兴趣,建议查阅原始源码。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。