原文链接:https://javascript.info/currying-partials,translate with ❤️ by zhangbao.

到现在为止,我们已经学到了绑定 this,让我们更深一步学习。

我们不仅可以绑定 this,也可以绑定参数。这很少使用,但是有时就是顺手。

bind 的完整语法是这样的:

  1. let bound = func.bind(context, arg1, arg2, ...);

这里不进位函数绑定了上下文环境,而且也绑定了参数。

例如,我们有一个乘法函数 mul(a, b):

  1. function mul(a, b) {
  2. return a * b;
  3. }

我们用 bind 来在此基础上,创建一个 double 函数:

  1. let double = mul.bind(null, 2);
  2. alert( double(3) ); // = mul(2, 3) = 6
  3. alert( double(4) ); // = mul(2, 4) = 8
  4. alert( double(5) ); // = mul(2, 5) = 10

调用 mul.bind(null, 2) 之后创建了一个新函数 double,这个函数的 this 绑定给了 null,并将 nul 的第一个参数绑定为 2。第二个参数就像之前一样再传递就是了。

这称为部分函数应用:我们根据已存在函数,固定了一些参数返回一个新函数。需要注意的是,这里我们并没有使用 this。但是 bind 函数需要这个参数,所以我们必须传递,这里我们传递了 null。

下面的 triple 函数用来扩大 3 倍值:

  1. let triple = mul.bind(null, 3);
  2. alert( triple(3) ); // = mul(3, 3) = 9
  3. alert( triple(4) ); // = mul(3, 4) = 12
  4. alert( triple(5) ); // = mul(3, 5) = 15

为什么我们会用到部分函数呢?

这里我们的好处是我们创建了一个具有可读名称的独立函数(double,triple)。我们可以使用它,不要每次都写第一个参数,因为它使用 bind 函数固定了第一个参数值。

在其他情况下,当我们有一个非常通用的函数时,部分应用程序是有用的,并且为了方便起见,需要一个不太通用的变体。

例如,我们有一个函数 send(from, to, text)。然后,在一个 user 对象中,我们可能想要使用它的变体,从当前用户发出 sendTo 函数请求。

实现没有指定上下文的部分函数

但是如果我们想固定参数,不想绑定 this 怎么办呢?

原生的 bind 并不允许这样。我们不能忽视环境变量,跳到参数绑定。

幸运的是,绑定参数的函数 partial 是很好实现的。

像这样:

  1. function partial(func, ...argsBound) {
  2. return function(...args) { // (*)
  3. return func.call(this, ...argsBound, ...args);
  4. }
  5. }
  6. // Usage:
  7. let user = {
  8. firstName: "John",
  9. say(time, phrase) {
  10. alert(`[${time}] ${this.firstName}: ${phrase}!`);
  11. }
  12. };
  13. // add a partial method that says something now by fixing the first argument
  14. user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
  15. user.sayNow("Hello");
  16. // Something like:
  17. // [10:00] John: Hello!

调用 partial(func[, arg1, arg2…]) 之后的结果是返回一个包装 (*) func 函数的新函数:

  • 能实现动态绑定 this(对 user.sayNow 调用来说是 user)。

  • 然后给定参数 …argsBound,这是调在用 partial 给的(”10:00)。

  • 然后给定参数 …args:通过返回的包装函数传递的(”Hello”)。

在结合了扩展运算符之后,很容易实现,对吧?

还有一个来自 lodash 库的 _.partial 方法实现。

Currying

有时候,人们会把上面提到的部分函数和另一个称为“Currying”搞混淆。这是另一个我们需要在这里提到的有趣的技术。

Currying 就是将一个以 f(a, b, c) 方式调用的函数转化为 f(a)(b)(c) 调用方式的技术。

下面我们将两元函数 curry 进行 currying 操作。也就是说,将 f(a,b) 转换为 f(a)(b) 的调用方式:

  1. function curry(func) {
  2. return function(a) {
  3. return function(b) {
  4. return func(a, b);
  5. };
  6. };
  7. }
  8. // usage
  9. function sum(a, b) {
  10. return a + b;
  11. }
  12. let carriedSum = curry(sum);
  13. alert( carriedSum(1)(2) ); // 3

可以看到,实现过程就是一系列的包装函数。

  • curry(func) 的调用结果是包装函数 function(a)。

  • 就像调用了 sum(1),参数被保存在了词法环境里,现在新的包装函数 function(b) 被返回。

  • 用参数 2 调用 function(b) 就像调用了 sum(1)(2), 它把调用传递给原来的多参数函数 sum。

更先进的 curring 的实现,来自 lodash 库的 _.curry 函数,他们返回一个包装器,当所有参数都被提供时,调调用函数,否则返回一个保存了已输入参数值的部分函数。

  1. function curry(f) {
  2. return function(...args) {
  3. // if args.length == f.length (as many arguments as f has),
  4. // then pass the call to f
  5. // otherwise return a partial function that fixes args as first arguments
  6. };
  7. }

Currying?是为了什么?

高级 Currying 既可以保持函数的正常调用,也可以很容易地从中获得部分函数。要理解这些好处,我们肯定需要一个有价值的真实例子。

例如,我们有记录和输出信息的日志函数 log(date, importance, message)。在实际项目中,这样的函数还有许多其他有用的特性,比如:通过网络或者发送或进行过滤。

  1. function log(date, importance, message) {
  2. alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
  3. }

我们来 curry!

  1. log = _.curry(log);

之后的 log 还可以像之前一样使用:

  1. log(new Date(), "DEBUG", "some debug");

但也可以用 curry 的形式调用:

  1. log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

让我们创建一个生成今天日志一个便利函数:

  1. // todayLog will be the partial of log with fixed first argument
  2. let todayLog = log(new Date());
  3. // use it
  4. todayLog("INFO", "message"); // [HH:mm] INFO message

现在,对于今天的调试消息来说,这是一个方便的函数:

  1. let todayDebug = todayLog("DEBUG");
  2. todayDebug("message"); // [HH:mm] DEBUG message

因此:

  1. 在 currying 之后,我们没有丢失任何东西,log 还可以像之前一样调用。

  2. 我们能够生成一个部分函数,在很多场景下都很方便。

高级 Currying 实现

如果你感兴趣的话,下面是我们可以使用的“高级”Currying 实现。

  1. function curry(func) {
  2. return function curried(...args) {
  3. if (args.length >= func.length) {
  4. return func.apply(this, args);
  5. } else {
  6. return function(...args2) {
  7. return curried.apply(this, args.concat(args2));
  8. }
  9. }
  10. };
  11. }
  12. function sum(a, b, c) {
  13. return a + b + c;
  14. }
  15. let curriedSum = curry(sum);
  16. // still callable normally
  17. alert( curriedSum(1, 2, 3) ); // 6
  18. // get the partial with curried(1) and call it with 2 other arguments
  19. alert( curriedSum(1)(2,3) ); // 6
  20. // full curried form
  21. alert( curriedSum(1)(2)(3) ); // 6

新的 Currying 实现看起来可能有些复杂,但实际上很容易理解。

curry(func) 函数调用的结果是返回了包装函数 curried:

  1. // func is the function to transform
  2. function curried(...args) {
  3. if (args.length >= func.length) { // 1
  4. return func.apply(this, args);
  5. } else {
  6. return function pass(...args2) { // 2
  7. return curried.apply(this, args.concat(args2));
  8. }
  9. }
  10. };

当我们运行它的时候,有两个分支:

  1. 现在调用:如果传递的参数 args 的数量大于等于原始函数中定义的(func.length),就用传递的参数调用函数。

  2. 得到部分函数:否则,func 就不调用。相反,返回另一个包装函数 pass,用当前给定的参数重新调用 curried 函数。再一次调用,我们会得到一个新的部分函数(如果参数不足够的话),否则就结束调用,直接用满足原始函数参数数量的参数们,调用原始函数,返回结果。

例如,让我们看看,针对 sum(a, b, c) 这样接收三个参数,Currying 之后,调用 curried(1)(2)(3) 的过程:

  1. 第一次调用 carried(1) 在词法环境里记住了 1,然后返回包装函数 pass。

  2. 然后包装函数 pass 用 2 调用:它使用了之前的参数(1)连接现在得到的参数 2,相当于调用了 curried(1)(2)

因为参数仍然小于 3 个,所以 curry 返回了 pass。

  1. 包装函数 pass 用参数 3 调用,pass(3) 携带之前的参数(1,2)并且连接 3,相当于调用 curried(1, 2, 3)——至少要有 3 个参数,他们都赋值给了原始函数。

如果这还不明显,只需在你的头脑或纸上追踪调用序列。

注:仅对固定参数数量的函数有效

Currying 要求函数具有已知的固定数量的参数。

注:比 Currying 还多一点

根据定义,Currying 应该将 sum(a, b, c) 转换为 sum(a)(b)(c)

但是,JavaScript 的大多数实现都是高级的,如前所述:它们还可以在多参数变体中保持函数的可调用性。

总结

  • 当我们修复现有函数的一些参数时,产生的(不那么普遍的)函数称为局部函数。我们可以用 bind 来获得局部函数,但也有其他的方法。

当我们不想一次又一次地重复同样的参数时,使用局部函数是很方便的。就像如果我们有一个 send(from, to) 函数,并且在我们的任务里,form 总是同一个值。我们可以得到一个局部函数,然后继续下去。

  • Currying 是使 f(a, b, c) 转换成能以 f(a)(b)(c) 方式调用的函数。JavaScript 实现通常都保持函数的可调用性,如果参数计数不够,则返回局部函数。

当我们想要简单的部分时,Currying 是一种很好的方式。正如我们在日志示例中所看到的:Currying之后的通用函数 log(date, importance, message) 在调用时给我们提供了一些参数,比如 log(date) 或两个参数 log(data, importance)。

(完)