函数组合
JavaScript中的函数组合简介。
背景
在函数式编程中,你通常会创建一些专用的函数来完成特定任务。在这种情况下,程序员依赖于抽象,并期望函数能够正常工作。然而,这种做法可能导致多个函数的链式调用,即一个接一个地调用许多函数。为了使代码更美观、让开发更轻松,可以使用函数组合。
函数组合简介
函数组合是一种方法,其中前一个函数的返回结果作为下一个函数的输入,再传递给下一个函数,直到执行到最后一个函数。这些函数可以由任意数量的函数组成。
在深入探讨函数组合之前,我们先学习一下柯里化函数的概念。
柯里化函数
柯里化函数的概念是指:一个函数只接受一个参数,并返回另一个也只接受一个参数的新函数。
请看下面的例子:
1var add = function (a){ 2 return function(b){ 3 return a + b; 4 } 5} 6// console.log(add(a,b)); <- 错误的调用方式 7// 逐次调用函数 8var func_add_2 = add(1); // 返回一个将1加到参数上的函数 9var final_output = func_add_2(5); 10console.log(final_output); 11// 更直接的方式 12final_output = add(1)(5); 13console.log(final_output);
在上面的代码中,函数 add 只接收一个参数,并返回另一个函数,这个新函数也只接收一个参数,并将该参数与 add 函数的参数相加。我们在第8行调用 add 函数,获取返回的函数,并在下一行调用它,然后将结果赋值给 final_output 变量。这种方式在示例中实现了将1和5相加。但第12行展示了一种更快捷的方法:在第一次调用时加上括号传入参数,然后立即调用返回的函数。我们称 add 函数为柯里化函数。
注意:
add函数接收一个参数并返回一个部分应用的结果。add函数的参数通过词法作用域被固定在返回的函数内部。
我们也可以对超过两个函数进行同样的操作。
1var add = function (a){ 2 return function(b){ 3 return function(c){ 4 return a + b + c; 5 } 6 } 7} 8// 通过连续三次调用函数 9var final_output = add(1)(5)(6); 10console.log(final_output);
在上述代码中,我们嵌套了三个函数,每个都是柯里化函数,除了最内层的函数。最终的调用出现在第9行。
柯里化函数在函数组合中特别有用。让我们回到函数组合的主题。
函数组合
传统上,函数组合可以通过以下两种形式实现。
1const double = x => x * 2 2const square = x => x * x 3// 传统方式 4var output1 = double(2); 5var output2 = square(output1); 6console.log(output2); 7// 变体二 8var output_final = square(double(2)); 9console.log(output_final);
在上面的代码中,为了对一个被加倍的数进行平方,我们需要先调用 double 函数,再调用 square 函数。这可以通过将结果分别赋值给变量并依次调用函数(第4至第5行)来实现,也可以采用更直接的方式(第8行)。
这两种方式虽然直观,但不够简洁。尝试使用 compose 函数,可以找到更优雅的解决方案。
1const compose = (f, g) => x => f(g(x)); // 两个函数的组合 2const double = x => x * 2 3const square = x => x * x 4 5// 函数组合 6var output_final = compose(square, double)(2); 7console.log(output_final);
这个例子引入了**compose 函数**。compose 函数接收两个函数作为参数,返回一个仅接受一个参数的单一函数。该参数通过依次调用传入 compose 的所有函数来处理。这提供了一个更清晰的接口,使得新的调用方式类似于第6行所示。
compose 函数
我们可以将 compose 函数进一步泛化,使其能接收任意数量的函数,并按顺序依次调用它们。
1const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
让我们逐步解析:
compose变量被赋值为一个函数。...fns使用扩展运算符将所有传入的参数打包成一个数组,并赋值给fns。compose函数返回另一个函数,该函数接收一个参数x。- 内部函数使用
x值和fns数组(函数列表),对fns数组使用reduceRight方法,初始值为x。 reduceRight方法接收一个函数,以x为初始参数调用,然后依次处理列表中的其余函数;下一个函数接收前一个函数的输出结果,依此类推。
上述过程展示了 compose 函数的工作原理。你可以传入任意数量的函数,它会返回一个函数,该函数按顺序链式调用所有传入的函数,起始值即为传入的参数。
下面看一个实际运行的例子:
1// 任意数量函数的组合 2const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x); 3const double = x => x * 2 4const square = x => x * x 5 6// 函数组合 7var output_final = compose(square, double)(2); 8console.log(output_final);
在上面的代码中,我们在第2行使用通用方法实现了 compose 函数。现在它可以接受任意数量的函数,输出结果与之前的实现一致。
pipe 函数
我们可以通过使用**pipe 函数**来反转函数的调用顺序。pipe 函数的实现如下:
1const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
实现方式与 compose 函数类似,只是使用了 reduce 方法而非 reduceRight,因此函数的调用顺序被反转。
1// 使用 pipe 实现任意数量函数的组合 2const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x); 3const double = x => x * 2 4const square = x => x * x 5 6// 函数管道 7var output_final = pipe(square, double)(2); 8console.log(output_final);
在上面的代码中,第2行使用通用方法实现了 pipe 函数。它现在也能接收任意数量的函数,与之前的 compose 函数类似,但使用的是 reduce 而非 reduceRight。由于调用顺序相反,这里的输出结果不同:square 函数在 double 函数之前被调用,而 compose 函数中则是相反的顺序。
函数组合的库支持
鉴于函数组合的重要性,我们可以使用已经实现 compose 和 pipe 函数的库,避免重复造轮子。以下是一些推荐的库:
这些库可以帮助你省去手动实现函数组合的麻烦,专注于实际用途。
本节课到此结束!