单步执行减少函数数组的函数时遇到问题

Having trouble stepping through function that reduces an array of functions

在函数数组上使用 reduce 方法时,我很难准确地跟踪 reduce 如何在数组上工作。

comboFunc(num, functionsArr) {
  return functionsArr.reduce(function (last, current) {
    return current(last);
  }, input);
}

所以 functionsArr = [add, multi] 和函数 addmulti

function add(num) {
  return num + 1;
}

function multi(num) {
  return num * 30;
}

函数 comboFunc 将:

comboFunc(2, functionsArr); //returns 90;

我了解 reduce 方法如何在数字数组上工作,通过对数组的每个元素应用一个函数,将数组折叠成一个值。这个使用 comboFunc 将函数数组作为参数的示例使用足够简单的数学,我可以理解 reduce 如何在数学上产生 90 的值,但我不理解逐行发生的事情以及从功能级别的每次迭代。我无法在脑海中清楚地表达出什么时候究竟发生了什么——2 的输入是如何从一个函数传递到另一个函数相对于闭包的,以及每个函数的顺序触发等等。

如果 reduce 方法将您传入的函数应用到数组的每个元素,并且当数组中的每个元素都是一个函数时...我如何从语法上写出我自己的理解?

传递给 reduce 方法的函数 returns current(last) 所以这是一个函数 returning 一个函数。那么这会使 last 等于 add 函数并且 current 等于 multi?换句话说 returning multi(add)?如果我的逻辑是正确的,这意味着发生的事情是输入然后被传递到 add 并计算,然后 returned 值被传递到 multi 并计算最终 return90?

按照您调用它的方式(使用两个参数),Array#reduce 为数组中的每个条目调用一次回调。

在第一次调用时,last 参数是您最后给出的值(input,在您的例子中),current 参数是第一个条目大批。您的回调正在调用传入 last 的条目(您的第一个函数),因此第一个函数是用 2 调用的。由于那是 add,调用它的结果是 3,然后您从回调中 return。

在第二次调用中,last 参数是您在上一次调用中return输入的;所以它是 3current 参数是数组中的下一个函数。在你的例子中,那是 multi,所以你用 3 来称呼它。 multireturns 90,你return.

reduce 的结果是回调 return 的最后一个值。所以,90,在你的例子中。

你可以这样想reduce的这种形式(这是不是字面上的意思,reduce更复杂;这是一个简化版):

// Again, this is a *simplified* example
function pseudoReduce(array, callback, initialValue) {
    var index, last;

    last = initialValue;
    for (index = 0; index < array.length; ++index) {
        last = callback(last, array[index], index, array);
    }
    return last;
}

Array#reduce 如果您没有给它第二个参数,那么在第一次传递时 会略有 不同;上面没有反映出来。)

了解正在发生的事情的最简单方法是将 console.log() 添加到您的代码中,以便您可以逐步查看正在发生的事情:

function comboFunc(num, functionsArr) {
  return functionsArr.reduce(function (last, current) {
    console.log(current + " " + last + " ==> " + current(last));
    return current(last);
  }, num);
}

function add(num) {
  return num + 1;
}

function multi(num) {
  return num * 30;
}

var functionsArr = [add, multi];
console.log( comboFunc(2, functionsArr) );

输出:

"function add(num) {
  return num + 1;
} 2 ==> 3"
"function multi(num) {
  return num * 30;
} 3 ==> 90"
90

这本质上是

multi(add(2)); // (2+1)*30 = 90

您似乎正在尝试创建一个 composition 函数,该函数采用一组函数应用于输入,num

让我们首先解决几个语法错误。首先,您缺少 function 关键字

// your code
comboFunc(num, functionsArr) {

// should be
function comboFunc(num, functionsArr) {

接下来,您将参数命名为 num,但随后在函数体

中将其引用为 input
// your code
}, input);

// should be
}, num);

因此,在您解决此问题后,您的功能可以正常工作,但如果我们以稍微不同的方式处理问题呢?

Disclaimer: this answer does not walk you through the function you've provided. Rather, it sympathizes with your difficulty by demonstrating comboFunc was written in an overly complex way. In turn, I give you an alternate definition of your function that operates identically but is a lot easier to read/understand.

首先,我认为我们应该回到函数组合的基础知识。

我喜欢把功能组合描绘成一张小地图。

  f(x)=y      g(y)=z
+--------> y -------+
|                   |
|                   |
|                   v
x ----------------> z
          ?

在上面的示例中,我们可以轻松地将 f 替换为您的 add 函数,将 g 替换为您的 multi 函数。定义 ? 现在应该也很明显了。

  add(2)      multi(3)
+--------> 3 -------+
|                   |
|                   |
|                   v
2 ----------------> 90
  h(x) = g(f(x))  = z
  h(x) = (g∘f)(x) = z
  h(2)            = 90

(g∘f) 可以说是“g of f”并且它只是意味着首先将 x 应用于 f,然后将该结果应用于 g.

注意 y 甚至不存在。我们已经根据 x 定义了 h,它给了我们一个直接 "map" 到 z,完全跳过 y

将两个函数组合在一起是一个非常基本的操作,我们可以定义一个函数来非常容易地做到这一点

function comp(g,f) {
  return function(x) {
    return g(f(x));
  }
}

var h = comp(multi,add);

   h(2);                   // 90
// h(2) = multi(add(2))    =  90

"Yeah, but I'm trying to work with N functions. Not just 2."

好的,comp 适用于两个函数,但我过度简化了问题。您可能想知道如果我们想将 3 个、4 个甚至更多的函数组合在一起,这将如何工作。

好吧再看一遍

a(w)              = x
b(x)              = y
c(y)              = z
d(w) = c(b(a(w))) = z
d(w) = (c∘b∘a)(w) = z

仔细观察 。我们知道这是我们的 comp 函数,我们可以看到它出现在我们的每个函数之间,abc.

在我们继续之前,假设我们有一个数字数组

var xs = [1,2,3];

如果我们想对数字求和,我们需要调用

var sum = 1 + 2 + 3

看到 + 是如何出现在每个数字之间的吗?使用我们的函数数组,没有什么不同!

var fs = [c,b,a];
var d  = (c ∘ b ∘ a)

我们只需要使它有效 JavaScript。这也很容易,因为我们已经将 定义为 comp.

在列表项之间调用函数称为线性 fold 并且 JavaScript 有一个函数调用 reduce

好吧,让我们开始使用吧!我们将使用 comp 函数

折叠 ("reduce") 我们的函数列表
function compN(fs) {
  return fs.reduce(comp);
}

var h = compN([multi, add]);

   h(2);                   // 90
// h(2) = multi(add(2))    =  90
// h(2) = (multi ∘ add)(2) =  90

好的,它适用于我们最初的两个函数,但我们要确保它适用于更长的列表

var i = compN([multi, add, add, add]);

   i(2);                               // 150
// i(2) = multi(add(add(add(2)))       =  150
// i(2) = (multi ∘ add ∘ add ∘ add)(2) =  150

Ok, so let's recap

function add(num) {
  return num + 1;
}

function multi(num) {
  return num * 30;
}

function comp(g,f) {
  return function(x) {
    return g(f(x));
  }
}

function compN(fs) {
  return fs.reduce(comp);
}

compN([multi, add])(2); // 90

"But why is this better?"

正如我们在 h(x) = (g∘f)(x) = z 中删除 y 一样,请注意我们的 compN 函数如何不关心 numlastcurrent。我们已经放弃了 3 个变量供我们的大脑思考,我们的功能在做什么更加清晰。

compfg 组合在一起得到 g ∘ f.

compN 在函数列表中的每个项之间调用 comp

对我来说,这非常简单易懂。只需查看每个 compcompN 的函数体,我就能立即确定其意图。

Compare that to

comboFunc(num, functionsArr) {
  return functionsArr.reduce(function (last, current) {
    return current(last);
  }, input);
}

难怪您难以理解它。 comboFunc 从一开始就担心自己太多,因为它试图成为两个操作而不是一个。

所以,我知道我没有向您介绍您的原始函数,但我希望在阅读此答案后您对函数组合和构建您自己的关注点分离函数有更深入的了解。


编辑

根据下面的一些讨论,我们的 compN 函数在空数组 [].

上工作很有价值
var f = compN([]);
// Uncaught TypeError: Reduce of empty array with no initial value

糟糕! 这里的预期行为是 compN 创建一个 returns 未修改输入的函数。

function id(x) {
  return x;
}

使用它作为 reduce 的起点将保证我们的 compN 函数即使在给定空数组时也能正常工作

// revised compN
function compN(fs) {
  return fs.reduce(comp, id);
}

看看

compN([])(2);           // 2
compN([multi, add])(2); // 90

现在 compN 将适用于包含 0 个或更多函数的数组。