函数式编程 - .bind.apply 用于 curry 函数

Functional Programming - .bind.apply for curry function

Reading about functional programming - 开始柯里化,示例有一个简单的柯里化函数。除了最后一个 else 块,我什么都懂。

var curry = function (fn, fnLength) {
    fnLength = fnLength || fn.length;
    return function () {
        var suppliedArgs = Array.prototype.slice.call(arguments);
        if (suppliedArgs.length >= fn.length) {
            return fn.apply(this, suppliedArgs);
        } else if (!suppliedArgs.length) {
            return fn;
        } else {
            return curry(fn.bind.apply(fn, [this].concat(suppliedArgs)), fnLength - suppliedArgs.length);
       }
    };
};

如果提供的参数是 >=,请使用提供的参数调用函数。

否则如果 suppliedArgs.length 是假的,return 原函数什么都不做。

还有呢???

先看看你是如何调用 Function#bind():

fun.bind(thisArg[, arg1[, arg2[, ...]]])

然后考虑你如何使用Function#apply():

fun.apply(thisArg, [argsArray])

所以给定 bind() 我们需要在一个函数上调用它,并给它多个参数(不是数组),我们所拥有的只是一个参数数组(suppliedArgs 在代码),那么我们该怎么做呢?好吧,调用一个接受多个参数而不是一个数组参数的函数的主要方法是在函数上使用 .apply()。那么我们有 fn.bind.apply(...something...).

.apply() 的第一个参数是 this 值——在 .bind() 的情况下,它需要是要绑定的函数(请参阅下文了解原因) .因此 fn.bind.apply(fn, ...).

然后,.apply() 的第二个参数是您正在调用的函数的所有参数的数组,在 .bind() 的情况下是 thisArg[, arg1[, arg2[, ...]]]。因此,我们需要一个数组,第一个值是函数内 this 的值,然后是其他参数。这就是 [this].concat(suppliedArgs) 产生的结果。

因此整个 fn.apply.bind(fn, [this].concat(suppliedArgs)) 生成了一个正确绑定的函数,该函数将向当前函数 "prefilled" 提供参数,并具有正确的 this 上下文。然后将生成的这个函数作为 fn 参数传递给 curry() 的递归调用,这又会以与顶级调用相同的方式生成另一个函数。

总体效果是,每当您调用由 curry() 创建的函数时,如果您没有传递预期数量的参数,您将获得一个采用剩余参数数量的新函数,或者您将使用正确传入的整个参数列表评估原始函数。

例如

function addAllNums(a, b, c, d, e) {
    return a + b + c + d + e;
}

var curriedAddAll = curry(addAllNums, 5);
var f1 = curriedAddAll(1); // produces a function expecting 4 arguments
var f2 = f1(2, 3); // f2 is a function that expects 2 arguments
var f3 = f2(4); // f3 is a function that expects 1 argument
var f4 = f3(5); // f4 doesn't expect any arguments
var ans = f4(); // ans = 1 + 2 + 3 + 4 + 5 = 15.
// OR var ans = f3(5); => same result

为什么 thisArg 值不同?

这行代码最令人困惑的地方可能是 .bind().apply().

thisArg 的两个不同值

对于 .apply()thisArg 是您希望 this 的值位于您调用 .apply() 的函数中的值。例如myFunction.apply(myObj, ['param1', 'param2']) 等同于 myObj.myFunction('param1', 'param2').

在这种特殊情况下,.bind()fn 函数上执行,因此我们希望 fn 成为 .bind()this 值,所以它知道它正在创建一个绑定版本的函数。

对于 .bind()thisArgthis 的值将在 returned 的绑定函数内。

在我们的例子中,我们想要 return 一个绑定函数,它具有与我们当前相同的 this 值。换句话说,我们希望在新函数中正确维护 this 值,这样它就不会在您创建新函数时丢失,这种情况在您调用柯里化函数时使用的参数少于预期时会发生。

如果我们没有正确维护 this 值,以下示例将不会记录 this 的正确值。但是通过维护它,会输出正确的值。

var myObj = { 
    a: 1, 
    b: curry(function (a, b, c, d) { 
           console.log('this = ', this); 
           return a + b + c + d; 
       })
};
var c = myObj.b(1,1,1); // c is a function expecting 1 argument
c(1); // returns 4, and correctly logs "this = Object {a: 1, b: function}"
      // if "this" wasn't maintained, it would log the value of "this" as
      // the global window object.

最后一个 else 块是 curry 函数的主要和最重要的部分,因为它是承载柯里化逻辑的实际行。

return curry(fn.bind.apply(fn, [this].concat(suppliedArgs)), fnLength - suppliedArgs.length);

这就是 returns 需要您之前函数的 n-1 个参数的新函数。为什么?它是多种事物的组合:

fn.bind.apply 只是在函数本身的上下文中调用函数,同时提供所需的参数 (suppliedArgs)。请注意 curry 的下一个参数是 fnLength - suppliedArgs.length,它减少了传递的参数所需的参数。

下面借助ES6来解释一下。事情会变得更加明显。

// Imagine we have the following code written in ES5
function fn(a, b, c) {
  console.log(a, b, c);
}

var arr = [1, 2, 3];
var funcWithBoundArguments = fn.bind.apply(fn, [null].concat(arr));

让我们将 ES5 代码转换为 ES6 代码

// ES6
function fn(a, b, c) { console.log(a, b, c) }

let arr = [1,2,3];

let funcWithBoundArguments = fn.bind(null, ...arr)

看到了吗?当您绑定一个函数时,我们必须显式枚举所有参数,例如:

fn.bind(null, 1, 2, 3)

但是如果我们事先不知道数组的内容,我们如何绑定数组的内容呢?

对,我们必须使用 .bind.apply() 其中:

  • apply 的第一个参数是我们绑定的函数 (fn)
  • 第二个参数是一个数组,它获取我们将函数绑定到的上下文(作为数组的第一项),数组的其余项是我们绑定到的参数(数字是可变的)我们的函数 (fn).