Lodash 源码阅读-flatRest
概述
flatRest 是 Lodash 内部的一个工具函数,它用于处理函数的剩余参数,并将剩余参数数组扁平化。这个函数在 Lodash 中广泛用于实现那些需要接收任意数量参数的 API,比如 _.pick、_.omit 等。
前置学习
依赖函数
- overRest: 用于处理函数的剩余参数,模拟 ES6 的 rest 参数功能
- setToString : 保持函数的
toString方法,用于调试和检查 - flatten: 将嵌套数组扁平化一层
技术知识
- 函数式编程: 高阶函数、函数组合
- 剩余参数 : ES6 的
...args语法在 ES5 环境中的模拟实现 - 函数元数据: 保持函数的字符串表示的重要性
- 数组扁平化: 处理嵌套数组结构
源码实现
js
function flatRest(func) {
return setToString(overRest(func, undefined, flatten), func + "");
}
实现思路
flatRest 函数的实现看似简单,但背后包含了几个关键步骤:
- 使用
overRest创建一个新函数,该函数能够将超出指定数量的参数收集到一个数组中 - 将这个参数数组通过
flatten函数扁平化一层 - 使用
setToString保持原函数的字符串表示,便于调试 - 返回这个增强后的新函数
这种实现方式使得 Lodash 的许多函数可以既接受参数列表,也接受数组参数,提高了 API 的灵活性。
源码解析
让我们对 flatRest 函数的实现进行更加深入的分析:
js
function flatRest(func) {
return setToString(overRest(func, undefined, flatten), func + "");
}
这个简短的函数实现了多层函数组合,我们从内到外逐层解析:
1. overRest 函数解析
js
overRest(func, undefined, flatten);
overRest 是 Lodash 内部实现的一个模拟 ES6 剩余参数(rest parameters)的函数。
- 作用:创建一个新函数,该函数会把超出特定数量的参数收集到一个数组中,然后对这个数组应用变换函数
- 参数 :
func:原始函数undefined:参数起始位置,这里传入undefined表示使用默认值,即func.length - 1(原函数参数数量减 1)flatten:应用于收集到的参数数组的变换函数
举个例子,假设我们有这样一个函数:
js
function exampleFunc(a, b) {
// 原始函数有2个命名参数
console.log(a, b, arguments[2], arguments[3]);
}
// 应用 overRest 后
var transformed = overRest(exampleFunc, undefined, flatten);
当调用 transformed(1, 2, [3, 4], 5) 时,内部处理过程大致如下:
- 确定参数收集点:
func.length - 1 = 2 - 1 = 1,即从第 2 个参数开始收集 - 前 1 个参数
1直接传给原函数,从第 2 个参数开始(2, [3, 4], 5)被收集成数组[2, [3, 4], 5] - 对收集的数组应用
flatten函数,得到[2, 3, 4, 5] - 最终调用
exampleFunc(1, [2, 3, 4, 5])
2. flatten 函数解析
flatten 函数负责将数组扁平化一层,处理嵌套数组的情况。
举例说明扁平化过程:
flatten([1, [2, 3], 4])→[1, 2, 3, 4]flatten([1, [2, [3, 4]], 5])→[1, 2, [3, 4], 5](只扁平化一层)
这种扁平化处理确保了无论用户传入参数列表还是数组,最终处理的结果都是一致的。
3. setToString 函数解析
js
setToString(newFunc, func + "");
setToString 函数用于保持函数的字符串表示,这看似是一个小细节,但在调试和函数检查时非常重要。
- 原理 :在 JavaScript 中,
Function.prototype.toString()会返回函数的源代码文本 - 问题:当我们用函数包装另一个函数时,原始函数的名称和结构信息会丢失
- 解决方案 :
setToString通过修改函数的toString方法,使其返回原始函数的字符串表示
例如,没有 setToString 时:
js
console.log(overRest(func, undefined, flatten).toString());
// 输出:可能是 "function anonymous() { ... }" 这样的形式
使用 setToString 后:
js
console.log(
setToString(overRest(func, undefined, flatten), func + "").toString()
);
// 输出:与原始 func 函数的 toString() 结果相同
4. 函数组合的数据流
整个 flatRest 函数体现了优雅的函数组合(function composition)模式,数据流向如下:
- 原始函数
func作为输入 - 通过
overRest转换为一个能处理剩余参数的新函数 - 在这个新函数中,剩余参数会通过
flatten进行扁平化 - 通过
setToString保持函数的字符串表示 - 返回最终的增强函数
这种嵌套调用方式是函数式编程的典型特征,每个函数负责一个明确的任务,然后通过组合实现更复杂的功能。
5. 性能考量
flatRest 的实现也考虑了性能因素:
- 只扁平化一层数组,避免了深度递归扁平化可能带来的性能问题
- 使用
overRest而非直接使用 ES6 的 rest 参数,保证了在旧环境中的兼容性 - 整个函数是一次性组合完成的,不需要多次遍历参数数组
应用示例
假设我们有一个使用 flatRest 的 pick 函数:
js
var pick = flatRest(function (object, paths) {
// 假设 basePick 是内部实现
return object == null ? {} : basePick(object, paths);
});
当用户这样调用时:
js
// 调用方式 1
pick({ a: 1, b: 2, c: 3 }, "a", "c");
// 调用方式 2
pick({ a: 1, b: 2, c: 3 }, ["a", "c"]);
// 调用方式 3(混合方式)
pick({ a: 1, b: 2, c: 3 }, "a", ["b", "c"]);
内部处理过程:
-
调用方式 1:
- 参数:
{a: 1, b: 2, c: 3}, 'a', 'c' overRest收集:第一个参数{a: 1, b: 2, c: 3}直接传递,其余参数['a', 'c']收集flatten处理:['a', 'c']已经是扁平的,不变- 最终调用:
basePick({a: 1, b: 2, c: 3}, ['a', 'c'])
- 参数:
-
调用方式 2:
- 参数:
{a: 1, b: 2, c: 3}, ['a', 'c'] overRest收集:第一个参数{a: 1, b: 2, c: 3}直接传递,其余参数[['a', 'c']]收集(注意这是个嵌套数组)flatten处理:[['a', 'c']]扁平化为['a', 'c']- 最终调用:
basePick({a: 1, b: 2, c: 3}, ['a', 'c'])
- 参数:
-
调用方式 3:
- 参数:
{a: 1, b: 2, c: 3}, 'a', ['b', 'c'] overRest收集:第一个参数{a: 1, b: 2, c: 3}直接传递,其余参数['a', ['b', 'c']]收集flatten处理:['a', ['b', 'c']]扁平化为['a', 'b', 'c']- 最终调用:
basePick({a: 1, b: 2, c: 3}, ['a', 'b', 'c'])
- 参数:
通过这样的处理,无论用户采用何种参数传递方式,最终都能得到一致的结果。
种调用方式都会得到相同的结果:{a: 1, c: 3}。
总结
flatRest 函数是 Lodash 内部用于增强 API 灵活性的重要工具。它通过组合 overRest、flatten 和 setToString 三个函数,实现了以下功能:
- 收集超出函数定义的参数
- 扁平化收集到的参数数组
- 保持函数的原始字符串表示
这样一来,Lodash 的许多函数可以同时支持传入参数列表和数组,提高了 API 的易用性。在自己设计函数库或 API 时,我们也可以借鉴这种设计模式,让函数接口更加灵活友好。