Lodash源码阅读-zipWith

156 阅读5分钟

Lodash 源码阅读-zipWith

概述

zipWith 是 Lodash 中的一个数组处理函数,用于将多个数组的对应元素重组并通过指定的迭代函数进行组合处理。它接收多个数组作为参数,最后一个参数可选地提供一个迭代函数,用于自定义如何组合每组对应元素。这个函数本质上是 _.zip 的增强版,提供了更灵活的数据处理能力。

前置学习

依赖函数

  • baseRest: 模拟 ES6 的剩余参数功能的内部工具函数,处理不定数量的参数
  • unzipWith: 将嵌套数组按列重组并应用迭代函数处理每个分组
  • identity: 返回输入值本身的函数,即 x => x

技术知识

  • 函数式编程: 理解高阶函数、函数组合和不可变数据操作
  • 数组转置: 理解矩阵转置的概念,即行列互换
  • 参数处理: 理解 JavaScript 中的可变参数处理技术
  • 函数判断: 如何判断参数是否为函数并据此做出不同处理

源码实现

var zipWith = baseRest(function (arrays) {
  var length = arrays.length,
    iteratee = length > 1 ? arrays[length - 1] : undefined;

  iteratee =
    typeof iteratee == "function" ? (arrays.pop(), iteratee) : undefined;
  return unzipWith(arrays, iteratee);
});

实现思路

zipWith 函数的实现采用了一种优雅的组合式设计:

  1. 使用 baseRest 处理可变数量的数组参数
  2. 检查最后一个参数是否为函数,如果是则将其作为迭代函数并从参数数组中移除
  3. 调用 unzipWith 将输入数组重组并应用迭代函数处理每个分组

这种设计使函数能够灵活处理不同数量的输入数组,并允许用户自定义如何组合对应位置的元素,非常适合数据转换和矩阵运算场景。

源码解析

1. 函数定义与参数收集

var zipWith = baseRest(function (arrays) {
  // ...
});

zipWith 使用 baseRest 包装,这是 Lodash 内部的一个工具函数,用于处理不定数量的参数。baseRest 模拟了 ES6 的 rest 参数功能,将所有传入的参数收集到一个名为 arrays 的数组中。

这种设计允许 zipWith 接收任意数量的数组参数,实现了灵活的函数调用方式:

zipWith([1, 2], [10, 20], [100, 200], function (a, b, c) {
  return a + b + c;
});
// 等价于
zipWith([1, 2], [10, 20], [100, 200], _.add);

2. 迭代函数处理

var length = arrays.length,
  iteratee = length > 1 ? arrays[length - 1] : undefined;

iteratee = typeof iteratee == "function" ? (arrays.pop(), iteratee) : undefined;

这段代码检查并提取可能作为最后一个参数传入的迭代函数:

  • 首先获取参数数组的长度
  • 暂时将最后一个参数赋值给 iteratee 变量
  • 检查这个参数是否为函数类型
    • 如果是函数,使用逗号运算符执行两个操作:从 arrays 数组中弹出这个函数,并将其赋值给 iteratee
    • 如果不是函数,将 iteratee 设置为 undefined

这种处理方式使得 zipWith 既可以接受迭代函数作为参数,也可以不提供迭代函数(此时会使用 unzipWith 的默认行为):

// 提供自定义迭代函数
zipWith([1, 2], [3, 4], function (a, b) {
  return a + b;
});
// => [4, 6]

// 不提供迭代函数
zipWith([1, 2], [3, 4]);
// => [[1, 3], [2, 4]]

3. 数组重组与处理

return unzipWith(arrays, iteratee);

最后,调用 unzipWith 函数完成重组和处理:

  • arrays 是收集到的所有数组参数(可能已弹出最后一个函数参数)
  • iteratee 是提取的迭代函数或 undefined

unzipWith 会将多个数组的对应元素组合在一起,然后对每组应用 iteratee 函数:

  1. 例如输入 [1, 2], [10, 20], [100, 200],会形成 [[1, 10, 100], [2, 20, 200]] 的分组
  2. 对每个分组应用 iteratee 函数,如果提供了 _.add,那么结果会是 [111, 222]

如果没有提供迭代函数(iterateeundefined),则直接返回重组后的数组,行为等同于 _.zip

示例分析

以下是两个实际的例子来说明 zipWith 的工作原理:

示例 1: 求和

_.zipWith([1, 2], [10, 20], [100, 200], function (a, b, c) {
  return a + b + c;
});
// => [111, 222]

执行流程:

  1. 参数 [1, 2], [10, 20], [100, 200] 和求和函数被收集到 arrays 数组
  2. 检测到最后一个参数是函数,从 arrays 中移除并赋值给 iteratee
  3. 调用 unzipWith 对剩余的数组进行处理,将对应位置的元素分组
  4. 对每个分组 [1, 10, 100][2, 20, 200] 应用求和函数
  5. 返回结果 [111, 222]

示例 2: 默认行为

_.zipWith(["a", "b"], [1, 2]);
// => [['a', 1], ['b', 2]]

执行流程:

  1. 参数 ['a', 'b'][1, 2] 被收集到 arrays 数组
  2. 最后一个参数不是函数,所以 iterateeundefined
  3. 调用 unzipWith 处理数组,但没有提供迭代函数
  4. unzipWith 进行简单的重组,没有额外处理
  5. 返回重组结果 [['a', 1], ['b', 2]]

总结

  1. 巧妙使用了函数组合设计,通过 baseRestunzipWith 复用已有功能,避免代码重复
  2. 灵活的参数处理机制使函数接口更加直观,同时兼容了多种调用方式
  3. 通过类型检查实现了可选的迭代函数,增强了函数的适用性和灵活性
  4. 采用了函数式编程的设计思想,使数据转换操作更加简洁明了