Lodash 源码阅读-baseFlatten
概述
baseFlatten
是 Lodash 内部的核心工具函数,用来实现数组扁平化操作。简单来说,它把多层嵌套的数组"压扁",变成层级更少或完全一维的数组。这个函数是 _.flatten
、_.flattenDeep
和 _.flattenDepth
等公开方法的基础实现。
前置学习
依赖函数
- isFlattenable :判断一个值是否可以被扁平化(是数组、arguments 对象或设置了
Symbol.isConcatSpreadable
的对象) - arrayPush:将一个数组的元素追加到另一个数组末尾
技术知识
- 递归:处理嵌套数组结构的关键技术
- 深度优先遍历:按深度处理嵌套数组的算法思想
- 数组操作:基本的数组遍历和元素添加
- 短路逻辑 :
&&
和||
运算符的条件判断和默认值设置 - 参数默认值:处理未提供的可选参数
源码实现
javascript
/**
* The base implementation of `_.flatten` with support for restricting flattening.
*
* @private
* @param {Array} array The array to flatten.
* @param {number} depth The maximum recursion depth.
* @param {boolean} [predicate=isFlattenable] The function invoked per iteration.
* @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.
* @param {Array} [result=[]] The initial result value.
* @returns {Array} Returns the new flattened array.
*/
function baseFlatten(array, depth, predicate, isStrict, result) {
var index = -1,
length = array.length;
predicate || (predicate = isFlattenable);
result || (result = []);
while (++index < length) {
var value = array[index];
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// Recursively flatten arrays (susceptible to call stack limits).
baseFlatten(value, depth - 1, predicate, isStrict, result);
} else {
arrayPush(result, value);
}
} else if (!isStrict) {
result[result.length] = value;
}
}
return result;
}
实现思路
baseFlatten
的核心思想是通过递归来处理嵌套数组。它接收五个参数:要扁平化的数组、扁平化深度、判断元素是否可扁平化的函数、是否严格模式以及存放结果的数组。
函数会遍历输入数组的每个元素,对于每个元素:
- 如果元素是可扁平化的(默认是数组或类数组)且扁平化深度大于 0:
- 如果深度大于 1,递归处理这个元素,深度减 1
- 如果深度等于 1,直接把元素内容添加到结果
- 如果元素不可扁平化或已达到最大深度,且不是严格模式,则直接将元素添加到结果
通过控制递归深度参数,就可以实现不同层级的扁平化效果。
源码解析
1. 参数初始化和默认值设置
javascript
var index = -1,
length = array.length;
predicate || (predicate = isFlattenable);
result || (result = []);
这段代码做了几件事:
- 初始化
index
为 -1,准备用于数组遍历(后面会使用前置递增++index
) - 获取数组长度
length
- 设置默认的判断函数,如果没有提供
predicate
,就使用isFlattenable
函数 - 设置默认的结果数组,如果没有提供
result
,就创建一个空数组
这里使用了 JavaScript 的短路逻辑 ||
来设置默认值。例如:
javascript
// 相当于
if (!predicate) {
predicate = isFlattenable;
}
2. 数组遍历和元素处理
javascript
while (++index < length) {
var value = array[index];
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// Recursively flatten arrays (susceptible to call stack limits).
baseFlatten(value, depth - 1, predicate, isStrict, result);
} else {
arrayPush(result, value);
}
} else if (!isStrict) {
result[result.length] = value;
}
}
这是函数的核心逻辑:
- 使用
while
循环遍历数组,++index
是前置递增,先加 1 再使用 - 获取当前元素
value
- 判断是否需要扁平化:
depth > 0 && predicate(value)
- 深度必须大于 0(还可以继续扁平化)
- 元素必须通过
predicate
检查(默认是检查是否为数组或类数组)
- 如果需要扁平化:
- 如果深度大于 1,递归调用
baseFlatten
,深度减 1 - 如果深度等于 1,使用
arrayPush
将元素内容添加到结果数组
- 如果深度大于 1,递归调用
- 如果不需要扁平化且不是严格模式,直接将元素添加到结果数组
注释中提到递归扁平化可能受调用栈限制,这是提醒对于非常深的嵌套数组可能会导致栈溢出。
3. isStrict 参数的作用与意义
在 baseFlatten
函数中,有一个关键的条件判断:
javascript
else if (!isStrict) {
result[result.length] = value;
}
这段代码处理的是那些不需要或不能被扁平化的元素。isStrict
参数决定了这些元素的命运:
- 当
isStrict
为false
(非严格模式,这是默认行为)时,这些元素会被保留在结果数组中 - 当
isStrict
为true
(严格模式)时,这些元素会被忽略,不会出现在结果中
严格模式的作用-与 JavaScript 的严格模式('use strict'
)完全是两个不同的概念
严格模式本质上是一个过滤器,它只保留那些能够被扁平化的元素(数组或类数组)的内容,而丢弃其他所有元素。这在某些场景下非常有用:
-
数据清洗:从混合数据中只提取数组元素
javascript// 使用严格模式 baseFlatten([1, [2, 3], "hello", [4, 5]], 1, isFlattenable, true, []); // 结果: [2, 3, 4, 5] // 1和"hello"被过滤掉了,因为它们不是数组 // 非严格模式(默认) baseFlatten([1, [2, 3], "hello", [4, 5]], 1, isFlattenable, false, []); // 结果: [1, 2, 3, "hello", 4, 5] // 所有元素都被保留
-
数据转换链:在函数式编程中,有时需要先映射数组元素,然后只保留数组结果
javascript// 简化版的实现 function flatMap(array, iteratee) { // 先对每个元素应用iteratee函数 const mapped = array.map(iteratee); // 然后使用严格模式扁平化,只保留数组结果 return baseFlatten(mapped, 1, isArray, true, []); } // 例如: flatMap([1, 2, 3], (x) => (x % 2 === 0 ? [x * 10] : x)); // 如果使用严格模式: [20](只保留了数组结果) // 如果不使用严格模式: [1, 20, 3](保留所有结果)
-
选择性收集:只收集满足特定条件的元素
javascript// 自定义判断函数,只扁平化包含偶数的数组 function hasEvenNumbers(value) { return Array.isArray(value) && value.some((x) => x % 2 === 0); } // 使用严格模式 baseFlatten([1, [2, 3], [5, 7], [4, 9]], 1, hasEvenNumbers, true, []); // 结果: [2, 3, 4, 9] // 只有[2, 3]和[4, 9]被扁平化,因为它们包含偶数 // [5, 7]不包含偶数,所以被过滤掉
为什么 Lodash 需要这个特性?
Lodash 的设计理念是提供灵活且可组合的工具函数。isStrict
参数让 baseFlatten
不仅可以用于简单的数组扁平化,还可以用于更复杂的数据转换和过滤操作。
在 Lodash 内部,这个参数被用于实现不同的功能:
_.flatten
,_.flattenDeep
等函数使用非严格模式,保留所有元素_.flatMap
,_.flatMapDeep
等函数在某些情况下会使用严格模式,实现更复杂的数据转换
实例说明:使用严格模式的效果
javascript
// 示例数据
const data = [1, [2, [3]], "hello", [4]];
// 非严格模式(默认)- 保留所有元素
baseFlatten(data, 1, isFlattenable, false, []);
// 结果: [1, 2, [3], 'hello', 4]
// 严格模式 - 只保留可扁平化元素的内容
baseFlatten(data, 1, isFlattenable, true, []);
// 结果: [2, [3], 4]
// 数字1和字符串'hello'被过滤掉了
可以看到,严格模式提供了一种强大的机制,让开发者可以在扁平化数组的同时进行数据过滤。这种灵活性是 Lodash 库设计的体现,使得一个基础函数可以服务于多种不同的应用场景。
4. 返回结果
javascript
return result;
函数最后返回扁平化后的结果数组。由于 result
数组是通过引用传递和修改的,所以递归过程中的所有操作都会影响最终结果。
实际运行示例
javascript
// 示例1:扁平化一层
baseFlatten([1, [2, [3, 4]]], 1);
// 执行过程:
// 初始: result = []
// 处理1: 不是数组,直接加入 result = [1]
// 处理[2, [3, 4]]: 是数组,depth=1,使用arrayPush
// 最终: result = [1, 2, [3, 4]]
// 示例2:扁平化两层
baseFlatten([1, [2, [3, 4]]], 2);
// 执行过程:
// 初始: result = []
// 处理1: 不是数组,直接加入 result = [1]
// 处理[2, [3, 4]]: 是数组,depth=2,递归处理
// -- 递归处理2: 不是数组,加入 result = [1, 2]
// -- 递归处理[3, 4]: 是数组,depth=1,使用arrayPush
// 最终: result = [1, 2, 3, 4]
总结
baseFlatten
函数是 Lodash 内部实现数组扁平化的基础工具,它通过灵活的参数设计实现了多种扁平化行为。这个函数设计体现了几个关键原则:
-
通用性和可复用性:
- 通过参数控制行为,一个函数满足多种扁平化需求
- 作为内部基础函数,被多个公开 API 复用
-
效率考虑:
- 直接操作数组索引而非使用 push 方法
- 通过引用传递结果数组,避免不必要的数组复制
-
递归与循环结合:
- 使用递归处理嵌套结构
- 使用循环处理同层元素
- 这种结合提供了良好的性能和灵活性
-
灵活的控制:
- 深度参数控制扁平化层级
- 判断函数参数自定义可扁平化条件
- 严格模式参数控制结果过滤
baseFlatten
虽简单,但却体现了函数式编程中组合小功能实现复杂逻辑的思想,以及如何设计一个既通用又高效的工具函数。