专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端早读课  ·  【图书】零基础开发AI ... ·  昨天  
前端早读课  ·  【第3462期】7 分钟深度理解柯里化 ·  昨天  
前端早读课  ·  【第3461期】多种序列帧格式的最佳实践 ·  2 天前  
前端大全  ·  vue实现预览编辑ppt、word、pdf、 ... ·  2 天前  
前端早读课  ·  【第3460期】如何在前端开发中实现零停机部署 ·  3 天前  
51好读  ›  专栏  ›  前端早读课

【第3462期】7 分钟深度理解柯里化

前端早读课  · 公众号  · 前端  · 2025-02-26 08:05

正文

前言

深入解释了 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 。它还是一个函数,仍然需要更多参数,但我们越来越接近了……







请到「今天看啥」查看全文