Lodash源码阅读-baseFlatten

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 的核心思想是通过递归来处理嵌套数组。它接收五个参数:要扁平化的数组、扁平化深度、判断元素是否可扁平化的函数、是否严格模式以及存放结果的数组。

函数会遍历输入数组的每个元素,对于每个元素:

  1. 如果元素是可扁平化的(默认是数组或类数组)且扁平化深度大于 0:
    • 如果深度大于 1,递归处理这个元素,深度减 1
    • 如果深度等于 1,直接把元素内容添加到结果
  2. 如果元素不可扁平化或已达到最大深度,且不是严格模式,则直接将元素添加到结果

通过控制递归深度参数,就可以实现不同层级的扁平化效果。

源码解析

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;
  }
}

这是函数的核心逻辑:

  1. 使用 while 循环遍历数组,++index 是前置递增,先加 1 再使用
  2. 获取当前元素 value
  3. 判断是否需要扁平化:depth > 0 && predicate(value)
    • 深度必须大于 0(还可以继续扁平化)
    • 元素必须通过 predicate 检查(默认是检查是否为数组或类数组)
  4. 如果需要扁平化:
    • 如果深度大于 1,递归调用 baseFlatten,深度减 1
    • 如果深度等于 1,使用 arrayPush 将元素内容添加到结果数组
  5. 如果不需要扁平化且不是严格模式,直接将元素添加到结果数组

注释中提到递归扁平化可能受调用栈限制,这是提醒对于非常深的嵌套数组可能会导致栈溢出。

3. isStrict 参数的作用与意义

baseFlatten 函数中,有一个关键的条件判断:

javascript 复制代码
else if (!isStrict) {
  result[result.length] = value;
}

这段代码处理的是那些不需要或不能被扁平化的元素。isStrict 参数决定了这些元素的命运:

  • isStrictfalse(非严格模式,这是默认行为)时,这些元素会被保留在结果数组中
  • isStricttrue(严格模式)时,这些元素会被忽略,不会出现在结果中

严格模式的作用-与 JavaScript 的严格模式('use strict')完全是两个不同的概念

严格模式本质上是一个过滤器,它只保留那些能够被扁平化的元素(数组或类数组)的内容,而丢弃其他所有元素。这在某些场景下非常有用:

  1. 数据清洗:从混合数据中只提取数组元素

    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]
    // 所有元素都被保留
  2. 数据转换链:在函数式编程中,有时需要先映射数组元素,然后只保留数组结果

    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](保留所有结果)
  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 内部实现数组扁平化的基础工具,它通过灵活的参数设计实现了多种扁平化行为。这个函数设计体现了几个关键原则:

  1. 通用性和可复用性

    • 通过参数控制行为,一个函数满足多种扁平化需求
    • 作为内部基础函数,被多个公开 API 复用
  2. 效率考虑

    • 直接操作数组索引而非使用 push 方法
    • 通过引用传递结果数组,避免不必要的数组复制
  3. 递归与循环结合

    • 使用递归处理嵌套结构
    • 使用循环处理同层元素
    • 这种结合提供了良好的性能和灵活性
  4. 灵活的控制

    • 深度参数控制扁平化层级
    • 判断函数参数自定义可扁平化条件
    • 严格模式参数控制结果过滤

baseFlatten 虽简单,但却体现了函数式编程中组合小功能实现复杂逻辑的思想,以及如何设计一个既通用又高效的工具函数。

相关推荐
xixixin_6 分钟前
css一些注意事项
前端·css
坊钰29 分钟前
【MySQL 数据库】增删查改操作CRUD(下)
java·前端·数据库·学习·mysql·html
excel32 分钟前
webpack 模块 第 六 节
前端
Watermelo61733 分钟前
Vue3+Vite前端项目部署后部分图片资源无法获取、动态路径图片资源报404错误的原因及解决方案
前端·vue.js·数据挖掘·前端框架·vue·运维开发·持续部署
好_快34 分钟前
Lodash源码阅读-flattenDepth
前端·javascript·源码阅读
好_快34 分钟前
Lodash源码阅读-baseWhile
前端·javascript·源码阅读
呆头呆脑~34 分钟前
阿里滑块 231 231纯算 水果滑块 拼图 1688滑块 某宝 大麦滑块 阿里231 验证码
javascript·爬虫·python·网络爬虫·wasm
好_快35 分钟前
Lodash源码阅读-flatten
前端·javascript·源码阅读
好_快35 分钟前
Lodash源码阅读-flattenDeep
前端·javascript·源码阅读
excel38 分钟前
webpack 模块图 第 一 节
前端