什么是高阶函数,为什么有人关心?

英文原文: https://jrsinclair.com/articles/2019/what-is-a-higher-order-function-and-why-should-anyone-care/

“高阶函数”是人们抛出的那些短语之一。但是,任何人都很难停下来解释这意味着什么。也许你已经知道什么是高阶函数。但是我们如何在现实世界中使用它们呢?什么时候以及如何有用的实际例子是什么?我们可以用它们来操纵DOM吗?或者,使用高阶函数的人是否炫耀?它们是否因为没有充分理由而使代码过于复杂?

我碰巧认为高阶函数是有用的。事实上,我认为它们是JavaScript作为一种语言最重要的特性之一。但在我们开始之前,让我们先来分解一下高阶函数。为此,我们从函数作为变量开始。

作为一等公民的Functions。

在JavaScript中,我们至少有三种不同的编写新函数的方法。首先,我们可以编写一个函数声明。例如:

// Take a DOM element and wrap it in a list item element.
function itemise(el) {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}

我希望这很熟悉。但是,您可能知道我们也可以将其写为函数表达式。这可能是这样的:

const itemise = function(el) {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}

然后,还有另一种方法来编写相同的函数:作为箭头函数:

const itemise = (el) => {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}

就我们的目的而言,所有三个功能基本相同。但请注意,最后两个示例将函数分配给变量。这似乎是一件小事。为什么不为变量赋一个函数?但这是一个大交易。JavaScript中的函数是“第一类”。也就是说,我们可以:

  1. 将函数分配给变量;
  2. 将函数作为参数传递给其他函数; 和从其他函数返回函数。

这很好,但是这与高阶函数有什么关系呢?好吧,注意最后两点。我们马上回过头来看看他们。同时,让我们看一些例子。

我们已经看到为变量分配函数。那么将它们作为参数传递呢?让我们编写一个可以与DOM元素一起使用的函数。如果我们运行,document.querySelectorAll()我们会返回一个NodeList而不是一个数组。NodeList没有.map()像数组那样的方法,所以让我们写一个:

在这个例子中,我们将itemise函数作为参数传递给elListMap函数。但我们可以使用我们的elListMap功能而不是创建列表。例如,我们可以使用它将类添加到一组元素中。

function addSpinnerClass(el) {
    el.classList.add('spinner');
    return el;
}

// Find all the buttons with class 'loader'
const loadButtons = document.querySelectorAll('button.loader');

// Add the spinner class to all the buttons we found.
elListMap(addSpinnerClass, loadButtons);

我们的elLlistMap函数将函数作为参数transform。这意味着我们可以重新使用该elListMap函数来执行一系列不同的任务。

我们现在已经看到了将函数作为参数传递的示例。但是从函数返回函数怎么样?那可能是什么样的?

让我们从编写常规旧函数开始。我们想要一个<li>元素列表并将它们包装在一个<ul>。不那么困难:

function wrapWithUl(children) {
    const ul = document.createElement('ul');
    return [...children].reduce((listEl, child) => {
        listEl.appendChild(child);
        return listEl;
    }, ul);
}

但是,如果我们后来有一堆段落元素我们想要包装<div>?没问题。我们也为此编写了一个函数:

function wrapWithDiv(children) {
    const div = document.createElement('div');
    return [...children].reduce((divEl, child) => {
        divEl.appendChild(child);
        return divEl;
    }, div);
}

这样可以正常工作。但是这两个功能看起来很强大。两者之间唯一有意义的变化是我们创建的父元素。

现在,我们可以编写一个带有两个参数的函数:父元素的类型和子元素列表。但是,还有另一种方法可以做到这一点。我们可以创建一个返回函数的函数。它可能看起来像这样:

function createListWrapperFunction(elementType) {
    // Straight away, we return a function.
    return function wrap(children) {
      // Inside our wrap function, we can 'see' the elementType parameter.
      const parent = document.createElement(elementType);
      return [...children].reduce((parentEl, child) => {
          parentEl.appendChild(child);
          return parentEl;
      }, parent);
    }
}

现在,这可能看起来有点复杂,所以让我们分解它。我们创建了一个除了返回另一个函数之外什么都不做的函数。但是,返回的功能记住的elementType参数。然后,稍后,当我们调用返回的函数时,它知道要创建什么类型的元素。所以,我们可以创建wrapWithUl并wrapWithDiv喜欢:

const wrapWithUl  = createListWrapperFunction('ul');
// Our wrapWithUl() function now 'remembers' that it creates a ul element.

const wrapWithDiv = createListWreapperFunction('div');
// Our wrapWithDiv() function now 'remembers' that it creates a div element.

返回的函数“记住”某些内容具有技术名称的业务。我们称之为封闭。关闭过于方便,但我们现在不会过多担心它们。

所以,我们已经看到:
1. 为变量分配函数;
1. 将函数作为参数传递; 和从另一个函数返回一个函数。

总而言之,拥有一流的功能似乎相当不错。但这与高阶函数有什么关系呢?好吧,让我们看看高阶函数的定义。

什么是高阶函数?

高阶函数是:

将函数作为参数或将函数作为结果返回的函数

听起来有点熟?在JavaScript中,函数是一等公民。短语“高阶函数”描述了利用此功能的函数。它并不多。这是一个简单概念的花哨的短语。

高阶函数的示例

一旦你开始寻找,你会看到整个地方的高阶函数。最常见的是接受函数作为参数的函数。所以我们先来看看。然后我们将介绍一些返回函数的函数的实际示例。

接受函数作为参数的函数

通过“回调”功能的任何地方,您都在使用高阶函数。这些在前端开发中无处不在。其中最常见的是.addEventListener()方法。当我们想要响应事件而采取行动时,我们会使用此功能。例如,如果我想制作一个按钮弹出警报:

function showAlert() {
  alert('Fallacies do not cease to be fallacies because they become fashions');
}

document.body.innerHTML += `<button type="button" class="js-alertbtn">
  Show alert
</button>`;

const btn = document.querySelector('.js-alertbtn');

btn.addEventListener('click', showAlert);

在此示例中,我们创建一个显示警报的函数。然后我们在页面上添加一个按钮。最后,我们将showAlert()函数作为参数传递给btn.addEventListener()。

当我们使用数组迭代方法时,我们也会看到高阶函数。也就是说,方法,如.map(),.filter()和.reduce()。我们已经看到了这个elListMap()功能:

function elListMap(transform, list) {
    return [...list].map(transform);
}

高阶函数也有助于我们处理延迟和时序。这些setTimeout()和setInterval()函数都可以帮助我们管理函数何时执行。例如,如果我们想在30秒后删除高亮类,我们可能会这样做:

function removeHighlights() {
    const highlightedElements = document.querySelectorAll('.highlighted');
    elListMap(el => el.classList.remove('highlighted'), highlightedElements);
}

setTimeout(removeHighlights, 30000);

同样,我们创建一个函数并将其作为参数传递给另一个函数。

如您所见,我们使用通常在JavaScript中接受函数的函数。事实上,你可能已经使用过它们了。

返回函数的函数

返回函数的函数不像接受函数的函数那样常见。但它们仍然有用。其中一个最有用的例子是maybe()功能。我从Reginald Braithewaite的JavaScriptAllongé改编了这个。它看起来像这样:

function maybe(fn)
    return function _maybe(...args) {
        // Note that the == is deliberate.
        if ((args.length === 0) || args.some(a => (a == null)) {
            return undefined;
        }
        return fn.apply(this, args);
    }
}

现在让我们先看看我们如何使用它,而不是解码它现在如何工作。让我们elListMap()再次检查一下我们的功能:

// Apply a given function to every item in a NodeList and return an array.
function elListMap(transform, list) {
    // list might be a NodeList, which doesn't have .map(), so we convert
    // it to an array.
    return [...list].map(transform);
}

如果我们偶然传递一个null或一个undefined值,会elListMap()发生什么?我们得到了一个TypeError,无论我们做什么,都会崩溃。该maybe()功能让我们解决这个问题。我们这样使用它:

const safeElListMap = maybe(elListMap);
safeElListMap(x => x, null);
// ← undefined

该函数返回而不是一切都崩溃undefined。如果我们将其传递给受maybe()… 保护的另一个函数,它将undefined再次返回。我们可以继续使用maybe()以保护我们喜欢的任何功能。比编写一个无数的if语句简单得多。

返回函数的函数在React社区中也很常见。例如,是一个返回函数的函数。

所以呢?

我们已经看到了一些高阶函数可以做的个别例子。但那又怎么样?他们给了我们什么,没有他们我们就没有?这里有一些比一些人为的例子更大的东西吗?

要回答这个问题,让我们再看一个例子。考虑内置数组方法.sort()。它有问题,是的。它会改变数组而不是返回一个新数组。但是让我们暂时忽略它。该.sort()方法是高阶函数。它需要一个函数作为其参数之一。

它是如何工作的?好吧,如果我们想对一组数字进行排序,我们首先要创建一个比较功能。它可能看起来像这样:

然后,为了对数组进行排序,我们像这样使用它:

let nums = [7, 3, 1, 5, 8, 9, 6, 4, 2];
nums.sort(compareNumbers);
console.log(nums);
// 〕[1, 2, 3, 4, 5, 6, 7, 8, 9]

我们可以对数字列表进行排序。但那有多大用处呢?我们多久有一个需要排序的数字列表?不常见。如果我需要对某些东西进行排序,那么它通常是一组对象。更像这样的东西:

let typeaheadMatches = [
    {
        keyword: 'bogey',
        weight: 0.25,
        matchedChars: ['bog'],
    },
    {
        keyword: 'bog',
        weight: 0.5,
        matchedChars: ['bog'],
    },
    {
        keyword: 'boggle',
        weight: 0.3,
        matchedChars: ['bog'],
    },
    {
        keyword: 'bogey',
        weight: 0.25,
        matchedChars: ['bog'],
    },
    {
        keyword: 'toboggan',
        weight: 0.15,
        matchedChars: ['bog'],
    },
    {
        keyword: 'bag',
        weight: 0.1,
        matchedChars: ['b', 'g'],
    }
];

想象一下,我们想要按weight每个条目对这个数组进行排序。好吧,我们可以从头开始编写新的排序功能。但我们不需要。相反,我们创建了一个新的比较函数。

function compareTypeaheadResult(word1, word2) {
    return -1 * compareNumbers(word1.weight, word2.weight);
}

typeaheadMatches.sort(compareTypeaheadResult);
console.log(typeaheadMatches);
// 〕[{keyword: "bog", weight: 0.5, matchedChars: ["bog"]}, … ]

我们可以为我们想要的任何类型的数组编写比较函数。该.sort()方法与我们达成协议。它说:“如果你能给我一个比较函数,我会对任何数组进行排序。不要担心数组中的内容。如果你给我一个比较函数,我会对它进行排序。“所以我们不必担心自己编写排序算法。我们专注于比较两个元素的更简单的任务。

现在,想象一下,如果我们没有高阶函数。我们无法将函数传递给该.sort()方法。每当我们需要对不同类型的数组进行排序时,我们就必须编写一个新的排序函数。或者,我们最终会用函数指针或对象重新发明相同的东西。无论哪种方式都会更加笨拙。

我们确实有更高阶的功能。这让我们将排序功能与比较功能分开。想象一下,如果一位聪明的浏览器工程师出现并更新.sort()以使用更快的算法。每个人的代码都会受益,无论他们排序的数组内部是什么。并且有一整套高阶数组函数遵循这种模式。

这带来了更广泛的想法。该.sort()方法抽象的任务排序从什么是远在阵列中。我们拥有所谓的“关注点分离”。高阶函数让我们创建笨拙或不可能的抽象。创建抽象是软件工程的80%。

每当我们重构代码以消除重复时,我们就会创建抽象。我们看到一个模式,并用该模式的抽象表示替换它。因此,我们的代码变得更简洁,更容易理解。至少,这就是主意。

高阶函数是创建抽象的强大工具。并且有一个与抽象相关的整个数学领域。它被称为类别理论。为了更准确,类别理论是关于发现抽象的抽象。换句话说,它是关于寻找模式的模式。在过去的70年左右,聪明的程序员一直在窃取他们的想法。这些想法显示为编程语言功能和库。如果我们学习这些模式模式,我们有时可以删除整个代码。或者将复杂问题简化为简单构建块的优雅组合。这些构建块是高阶函数。这就是高阶函数很重要的原因。

英文原文: https://jrsinclair.com/articles/2019/what-is-a-higher-order-function-and-why-should-anyone-care/

李, 国轩