前言
深入解释了 JavaScript 中的 currying 技术,并通过示例和调试步骤展示了如何实现和使用。今日前端早读课文章由 @Yazeed Bzadough 分享,@飘飘翻译。
译文从这开始~~
埃里克・埃利奥特(Eric Elliott)出色的《软件创作》系列文章最初让我对函数式编程产生了兴趣。这绝对值得一读。
在该系列的某个地方,他提到了柯里化。计算机科学和数学对它的定义是一致的:
柯里化将多参数函数转换为一元(单参数)函数。
柯里化函数每次只接受一个参数。所以如果你有
greet = (greeting, first, last) => `${greeting}, ${first}${last}`;
greet('Hello', 'Bruce', 'Wayne'); // Hello, Bruce Wayne
正确地对
greet
进行柯里化会给你
curriedGreet = curry(greet);
curriedGreet('Hello')('Bruce')('Wayne'); // Hello, Bruce Wayne
这个三参数函数已被转换为三个一元函数。当你提供一个参数时,就会弹出一个新的函数,期待下一个参数。
正常吗?
我之所以说 “正确地柯里化”,是因为有些函数在使用上更加灵活。柯里化在理论上很棒,但在 JavaScript 中为每个参数调用一次函数会让人感到厌烦。
Ramda 的
curry
函数允许您像这样调用
curriedGreet
:
// greet requires 3 params: (greeting, first, last)
// these all return a function looking for (first, last)
curriedGreet('Hello');
curriedGreet('Hello')();
curriedGreet()('Hello')()();
// these all return a function looking for (last)
curriedGreet('Hello')('Bruce');
curriedGreet('Hello', 'Bruce');
curriedGreet('Hello')()('Bruce')();
// these return a greeting, since all 3 params were honored
curriedGreet('Hello')('Bruce')('Wayne');
curriedGreet('Hello', 'Bruce', 'Wayne');
curriedGreet('Hello', 'Bruce')()()('Wayne');
请注意,你可以一次性给出多个参数。这种实现方式在编写代码时更有用。
正如上面所展示的,你可以无限次地调用此函数而不传入参数,它始终会返回一个期望剩余参数的函数。
这怎么可能?
埃利奥特先生分享了一个与 Ramda 类似的
curry
实现。下面是代码,或者正如他恰如其分地称呼的那样,一个魔法咒语:
const curry = (f, arr = []) => (...args) =>
((a) => (a.length === f.length ? f(...a) : curry(f, a)))([...arr, ...args]);
Umm… 😐
Yeah, 我知道…… 这非常简洁,所以咱们一起重构并好好欣赏一下吧。
此版本功能相同
我还添加了一些
debugger
语句,以便在 Chrome 开发者工具中对其进行检查。
curry = (originalFunction, initialParams = []) => {
debugger;
return (...nextParams) => {
debugger;
const curriedFunction = (
params) => {
debugger;
if (params.length === originalFunction.length) {
return originalFunction(...params);
}
return curry(originalFunction, params);
};
return curriedFunction([...initialParams, ...nextParams]);
};
};
打开开发者工具,跟着操作吧!
我们开始吧!
在控制台中粘贴
greet
和
curry
。然后输入
curriedGreet = curry(greet)
,开始疯狂之旅。
暂停第 2 行
查看我们的两个参数,我们看到
originalFunction
是
greet
,而
initialParams
因为我们没有提供所以默认为空数组。移动到下一个断点,哦,等等…… 就这样了。
【第1253期】柯里化函数应用
没错!
curry(greet)
只是返回一个新的函数,该函数还需要 3 个参数。在控制台输入
curriedGreet
来看看我说的是什么。
玩够那个之后,咱们来点疯狂的,试试
sayHello = curriedGreet('Hello')
。
暂停第 4 行
在继续之前,请在控制台中输入
originalFunction
和
initialParams
。请注意,尽管我们处于一个全新的函数中,但仍能访问这两个参数?这是因为从父函数返回的函数可以访问其父函数的作用域。
现实生活中的继承
父函数传递下去之后,会把参数留给子函数使用。这有点像现实生活中的继承。
最初,
curry
被赋予了
originalFunction
和
initialParams
,然后返回了一个 “子” 函数。那两个变量尚未被释放,因为也许那个子函数还需要它们。如果不需要,那么那个作用域就会被清理掉,因为当没有人在引用你的时候,你才算真正 “死亡”。
好了,回到第 4 行……
检查一下
nextParams
,看看它是不是
['Hello']
…… 一个数组?但我记得我们说的是
curriedGreet(‘Hello’)
,不是
curriedGreet(['Hello'])
!
正确:我们用
Hello
调用了
curriedGreet
,但由于剩余参数语法,我们将
Hello
转换为了
['Hello']
。
Y THO?!
curry 是一个通用函数,可以接受 1 个、10 个或 1000 万个参数,因此它需要一种方法来引用所有这些参数。像这样使用剩余语法可以将每个参数都捕获到一个数组中,从而使
curry
的工作变得容易得多。
让我们跳转到下一个
debugger
语句。
现在是第 6 行,但请稍等。
你可能已经注意到,第 12 行实际上是在第 6 行的
debugger
语句之前运行的。如果没有注意到,请仔细查看。我们的程序在第 5 行定义了一个名为
curriedFunction
的函数,在第 12 行使用了它,然后在第 6 行遇到了那个
debugger
语句。那么,
curriedFunction
是用什么调用的呢?
[...initialParams, ...nextParams];
没错。看看第 5 行的
params
,你会看到
['Hello']
。由于
initialParams
和
nextParams
都是数组,所以我们使用方便的扩展运算符将它们展平并合并成了一个数组。
精彩之处就在这里。
第 7 行说:“如果
params
和
originalFunction
长度相同,就用我们的参数调用
greet
,这样就完成了。” 这让我想起来……
JavaScript 函数也有长度属性
这就是
curry
发挥魔力的方式!这就是它决定是否要求更多参数的方法。
在 JavaScript 中,函数的
.length
属性会告诉你它期望的参数数量。
greet.length; // 3
iTakeOneParam = (a) => {};
iTakeTwoParams = (a, b) => {};
iTakeOneParam.length; // 1
iTakeTwoParams.length; // 2
如果提供的参数和预期的参数匹配,那就没问题了,直接把它们交给原始函数,完成任务!
这是 baller🏀
但在我们的例子中,参数和函数长度并不相同。我们只提供了
Hello
,所以
params.length
为 1,而
originalFunction.length
为 3,因为
greet
需要 3 个参数:
greeting
,
first
,
last
。
那么接下来会发生什么呢?
由于那个
if
语句的计算结果为
false
,代码将跳转到第 10 行并重新调用我们的主
curry
函数。它再次接收
greet
,这次
Hello
,然后又重新开始这一疯狂的过程。
【第1453期】理解JavaScript的柯里化
这就是递归,朋友们。
curry 本质上就是一个永无止境的自我调用循环,这些函数对参数如饥似渴,直到满足了它们的 “客人” 才会罢休。这便是极致的 “好客之道”。
回到第 2 行
参数与之前相同,只是这次
initialParams
是
['Hello']
。再次跳过以退出循环。在控制台中输入我们的新变量
sayHello
。它还是一个函数,仍然需要更多参数,但我们越来越接近了……