Lodash 源码阅读-chunk
功能概述
chunk
函数是 Lodash 库中的一个实用工具函数,用于将数组拆分成多个长度为指定大小的小数组。如果原数组不能被平均分割,则最后一个小数组将包含剩余的元素。
前置学习
依赖函数
在理解 chunk
函数之前,建议先了解它所依赖的以下函数:
- baseSlice :Lodash 内部函数,用于创建数组的切片,是
chunk
实现的核心 - isIterateeCall:内部函数,用于检测参数是否为迭代回调
- toInteger:将值转换为整数
- nativeMax :原生
Math.max
函数的引用 - nativeCeil :原生
Math.ceil
函数的引用
技术知识
- 数组切片:理解如何从数组中提取部分元素
- 向上取整 :使用
Math.ceil
计算需要创建的数组数量 - 参数处理:处理可选参数和默认值
- 数组遍历:使用循环构建结果数组
源码实现
javascript
/**
* Creates an array of elements split into groups the length of `size`.
* If `array` can't be split evenly, the final chunk will be the remaining
* elements.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to process.
* @param {number} [size=1] The length of each chunk
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the new array of chunks.
* @example
*
* _.chunk(['a', 'b', 'c', 'd'], 2);
* // => [['a', 'b'], ['c', 'd']]
*
* _.chunk(['a', 'b', 'c', 'd'], 3);
* // => [['a', 'b', 'c'], ['d']]
*/
function chunk(array, size, guard) {
if (guard ? isIterateeCall(array, size, guard) : size === undefined) {
size = 1;
} else {
size = nativeMax(toInteger(size), 0);
}
var length = array == null ? 0 : array.length;
if (!length || size < 1) {
return [];
}
var index = 0,
resIndex = 0,
result = Array(nativeCeil(length / size));
while (index < length) {
result[resIndex++] = baseSlice(array, index, (index += size));
}
return result;
}
源码解析
1. 参数处理
javascript
if (guard ? isIterateeCall(array, size, guard) : size === undefined) {
size = 1;
} else {
size = nativeMax(toInteger(size), 0);
}
这段代码处理 size
参数:
- 如果提供了
guard
参数且isIterateeCall(array, size, guard)
为真,或者size
未定义,则将size
设为默认值 1 - 否则,将
size
转换为整数,并确保它不小于 0(使用nativeMax
取toInteger(size)
和 0 的最大值)
guard
参数是一个内部参数,用于支持将 chunk
作为迭代函数用于 _.map
等方法。isIterateeCall
检查参数是否为迭代回调的一部分,这是 Lodash 内部的优化机制。
guard
参数和 isIterateeCall
函数详解
guard
参数和 isIterateeCall
函数是 Lodash 内部的一种机制,用于处理函数作为迭代器回调时的参数解析。这可能是 Lodash 中较为晦涩的部分,让我们详细解释:
-
guard
参数的作用:guard
参数不是给普通用户使用的,而是 Lodash 内部使用的一个标记参数- 它的存在使得 Lodash 函数可以被用作其他高阶函数(如
_.map
、_.forEach
等)的回调函数 - 注意源码中的
@param-
标记(带有破折号),这表明该参数不是公开文档的一部分
-
isIterateeCall
函数的作用:- 它检查当前函数调用是否是作为迭代回调(iteratee)的一部分
- 具体来说,它会判断传入的参数是否符合作为迭代回调时的特定模式
- 这样可以根据调用上下文来调整函数的行为
-
实际应用场景:
当
chunk
函数被用作迭代回调时,它可能会收到额外的参数。例如:javascript_.map( [ [1, 2, 3], [4, 5, 6], ], _.chunk );
在这种情况下,
chunk
会被这样调用:chunk(value, index, collection)
,其中:value
是当前元素,例如[1, 2, 3]
index
是当前索引,例如0
collection
是整个集合
没有
guard
参数和isIterateeCall
检查,chunk
会将index
(一个数字)解释为size
参数,这不是我们想要的行为。
2. 边界检查
javascript
var length = array == null ? 0 : array.length;
if (!length || size < 1) {
return [];
}
这部分进行边界检查:
- 获取数组长度,如果数组为
null
或undefined
,则长度为 0 - 如果数组长度为 0 或
size
小于 1,则直接返回空数组
这种防御性编程确保了函数能够安全地处理各种边缘情况,避免运行时错误。
3. 结果数组初始化
javascript
var index = 0,
resIndex = 0,
result = Array(nativeCeil(length / size));
初始化结果数组:
index
用于跟踪原数组的当前位置resIndex
用于跟踪结果数组的当前位置result
是预先分配好大小的结果数组,大小为Math.ceil(length / size)
使用 Math.ceil
向上取整确保了即使最后一个小数组不满 size
个元素,也能为其分配空间。预先分配数组大小比动态增长数组更高效,避免了多次内存重新分配。
4. 数组分块实现
javascript
while (index < length) {
result[resIndex++] = baseSlice(array, index, (index += size));
}
这是函数的核心逻辑,使用循环将原数组分成多个小数组:
- 循环继续,直到处理完原数组中的所有元素
- 每次迭代,使用
baseSlice
从原数组中提取从index
到index + size
的元素(不包括index + size
) index += size
将index
增加size
,同时作为baseSlice
的结束索引- 将提取的小数组存储在结果数组的
resIndex
位置,并将resIndex
增加 1
baseSlice
函数是 Lodash 内部的工具函数,用于创建数组的切片,类似于原生的 Array.prototype.slice
,但有更好的性能和更一致的行为。
执行流程示例
以 _.chunk(['a', 'b', 'c', 'd', 'e'], 2)
为例,执行流程如下:
- 参数处理:
size = 2
- 边界检查:数组长度为 5,
size
为 2,继续执行 - 初始化:
index = 0
resIndex = 0
result = Array(Math.ceil(5/2)) = Array(3)
(预分配长度为 3 的数组)
- 第一次循环:
result[0] = baseSlice(['a', 'b', 'c', 'd', 'e'], 0, 2)
result[0] = ['a', 'b']
index = 2
resIndex = 1
- 第二次循环:
result[1] = baseSlice(['a', 'b', 'c', 'd', 'e'], 2, 4)
result[1] = ['c', 'd']
index = 4
resIndex = 2
- 第三次循环:
result[2] = baseSlice(['a', 'b', 'c', 'd', 'e'], 4, 6)
result[2] = ['e']
(最后一个块包含剩余元素)index = 6
resIndex = 3
index > length
,循环结束,返回result = [['a', 'b'], ['c', 'd'], ['e']]
总结
chunk
函数是 Lodash 中一个简单但非常实用的工具函数,它通过将数组分割成指定大小的小数组,解决了许多实际开发中的常见问题。其设计体现了几个重要的软件工程原则:
-
简单性:
- 函数接口简洁明了,只需要两个主要参数:数组和大小
- 实现逻辑直观,易于理解和维护
-
健壮性:
- 处理各种边缘情况,如空数组、无效的
size
值 - 使用防御性编程风格,避免运行时错误
- 处理各种边缘情况,如空数组、无效的
-
效率:
- 预先分配结果数组的大小,避免动态增长带来的性能开销
- 使用内部优化的
baseSlice
函数,而不是原生的Array.prototype.slice
-
灵活性:
- 支持作为迭代函数用于其他 Lodash 方法
- 处理各种类型的输入,包括非数组值和非数字的
size