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 时,我们也可以借鉴这种设计模式,让函数接口更加灵活友好。