网络埋伏纪事

组合软件:4. 高阶函数

网络埋伏纪事 · 2017-05-04翻译 · 611阅读 原文链接

高阶函数是以一个函数为实参,或者返回一个函数的函数。高级函数与一阶函数相反,一阶函数不能以一个函数为实参或者返回一个函数为输出的。

之前我们看到过 .map().filter() 的例子。二者都以一个函数作为实参。二者都是高阶函数。

下面我们来看一个一阶函数的例子。这个函数会从单词列表中过滤掉所有 4 个字母的单词:

const censor = words => {
  const filtered = [];
  for (let i = 0, { length } = words; i < length; i++) {
    const word = words[i];
    if (word.length !== 4) filtered.push(word);
  }
  return filtered;
};
censor(['oops', 'gasp', 'shout', 'sun']);
// [ 'shout', 'sun' ]

如果想选中以 's' 开头的所有单词该怎么办呢?我们可以创建另一个函数:

const startsWithS = words => {
  const filtered = [];
  for (let i = 0, { length } = words; i < length; i++) {
    const word = words[i];
    if (word.startsWith('s')) filtered.push(word);
  }
  return filtered;
};
startsWithS(['oops', 'gasp', 'shout', 'sun']);
// [ 'shout', 'sun' ]

你可能已经看出这两个函数有不少重复的代码。这里就有一个模式形成了,它可以被抽象成更通用的解决方案。这两个函数有一个共同点:它们都遍历一个列表,并用给定的条件过滤列表。

迭代和过滤看起来好像它们都在求着被抽象出来,这样就可以被共享和重用,从而可以创建各种类似的功能。毕竟,从列表中选择条目是很常见的任务。

幸运的是,JavaScript 有头等函数。这是啥意思呢?就跟数字、字符串或者对象一样,函数可以:

  • 赋值给一个标识符(变量)
  • 赋值给对象的属性
  • 传递为实参
  • 从函数返回

从本质上说,我们可以像用程序中其它数据一样来用函数,而这让抽象变得更简单。例如,我们可以创建一个函数,通过给它传递一个处理不同之处的函数(我们称这个函数为 reducer )作为参数,把遍历一个列表以及累加返回值的过程抽象出来:

const reduce = (reducer, initial, arr) => {

  // 共享的东西
  let acc = initial;
  for (let i = 0, length = arr.length; i < length; i++) {
    // 特殊的东西放在 reducer() 调用中
    acc = reducer(acc, arr[i]);
  // 更多共享的东西
  }
  return acc;
};

reduce((acc, curr) => acc + curr, 0, [1,2,3]); // 6

这个 reduce() 实现以一个 reducer 函数、一个用于累加器的初始值 initial,和一个要遍历的数组 arr 为参数。对于数组中的每一个条目,调用 reducer,将累加器和当前数组元素作为参数传递给 reducer 函数。返回值被赋值给累加器。当完成对列表中所有值应用 reducer 后,返回累加值。

在使用示例中,我们调用 reduce,向它传递第一个参数:函数 (acc, curr) => acc + curr。这个函数以累加器和数组的当前值作为参数,返回一个新的累加值。然后,我们传进一个初始值 0,最后传进要遍历的数据。

将遍历和值累加抽象出来后,现在我们可以实现一个更通用的 filter() 函数了:

const filter = (
  fn, arr
) => reduce((acc, curr) => fn(curr) ?
  acc.concat([curr]) :
  acc, [], arr
);

filter() 函数中,除了传进来作为参数的 fn() 函数以外,一切都是共享的。这个 fn() 参数称为一个断言。断言(predicate)是返回一个布尔值的函数。

我们用当前值来调用 fn(),并且如果 fn(curr) 测试返回 true,就把 curr 值合并到累加器数组。否则,就只返回当前累加器值。

现在我们可以用 filter() 来实现 censor(),过滤掉 4个字符的单词:

const censor = words => filter(
  word => word.length !== 4,
  words
);

哇!所有通用的东西被抽象出来后,censor() 就成了一个很小的函数。

startsWithS() 也是如此:

const startsWithS = words => filter(
  word => word.startsWith('s'),
  words
);

如果你关注的话,可能已经知道 JavaScript 已经为我们实现了这种抽象。我们有了 Array.prototype 方法、.reduce().filter().map() 以及更多好的措施。

高阶函数还常用于对如何操作不同数据类型进行抽象。例如,.filter() 不必只操作字符串数组。它可以很容易过滤数字,因为你可以传递进一个函数,而该函数知道如何处理不同的数据类型。还记得 highpass() 的例子吧?

const highpass = cutoff => n => n >= cutoff;
const gt3 = highpass(3);
[1, 2, 3, 4].filter(gt3); // [3, 4];

换句话说,你可以用高阶函数来让一个函数多态化。正如你所见,高阶函数比一阶函数可重用和通用得多。一般来说,我们会在实际应用程序代码中组合使用高阶函数和很简单的一阶函数。

相关文章