功能JavaScript:什么是高阶函数,为什么需要它们?


“高阶函数”是经常分散的那些短语之一。 但是很少有人能停下来解释它的含义。 您可能已经知道所谓的高阶函数。 但是我们如何在实际项目中使用它们? 他们何时,为何有用? 我们可以在他们的帮助下操纵DOM吗? 还是使用这些功能的人会炫耀? 也许他们毫无意义地使代码复杂化了?

我以前认为高阶函数很有用。 现在,我认为它们是JavaScript作为一种语言的最重要属性。 但是在讨论这一点之前,让我们首先弄清楚什么是高阶函数。 我们将从函数作为变量开始。

用作一流对象


在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中的函数属于“ 第一类 ”。 因此,我们可以:

  • 将函数分配给变量。
  • 将函数作为参数传递给其他函数。
  • 从其他函数返回函数。

这很棒,但是所有这些与高阶函数有什么关系呢? 注意最后两点。 很快我们将返回它们,但是现在让我们来看一些示例。

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

 // 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); } // Grab all the spans on the page with the class 'for-listing'. const mySpans = document.querySelectorAll('span.for-listing'); // Wrap each one inside an <li> element. We re-use the // itemise() function from earlier. const wrappedList = elListMap(itemise, mySpans); 

在这里,我们将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将另一个函数作为参数进行转换。 也就是说,我们可以使用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参数。 然后,当我们调用返回的函数时,它已经知道要创建哪个元素。 因此,您可以创建wrapWithUlwrapWithDiv

 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. 

当返回的函数“记住”某些东西时,这个技巧称为closure 。 您可以在此处阅读有关它们的更多信息。 闭包非常方便,但就目前而言,我们不会考虑它们。

因此,我们进行了整理:

  • 将函数分配给变量。
  • 将函数作为参数传递。
  • 从另一个函数返回一个函数...

通常,头等舱的功能是一件令人愉快的事情。 但是高阶函数与它什么关系呢? 让我们看一下定义。

什么是高阶函数?


定义这是一个将函数作为参数或返回结果的函数。

熟悉吗? 在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().filter() .reduce() 。 像在elListMap()函数中一样:

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

高阶功能还有助于延迟和定时。 setTimeout()setInterval()函数有助于控制何时执行函数。 例如,如果您需要在30秒后删除highlight类,则可以这样操作:

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

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

如您所见,JavaScript通常具有接受其他功能的功能。 您可能已经在使用它们。

函数返回函数


这种功能并不像以前那样常见。 但是它们也很有用。 最好的例子之一是may()函数。 我改编了AllongéJavaScript书中的一个变体:

 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()看一下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或未定义的值传递给elListMap()什么? 不管发生什么,我们都会得到TypeError和当前操作的失败。 可以使用maybe()函数避免这种情况:

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

该函数将返回undefined ,而不是下降。 如果将其传递给maybe()maybe()保护的另一个函数,则将再次变得undefinedmaybe()可以保护任意数量的函数,编写十亿条if要容易得多。

返回函数的函数在React世界中也很常见。 例如, connect()

那接下来呢?


我们看到了一些使用高阶函数的示例。 那接下来呢? 他们能给我们什么,没有他们我们无法获得的?

为了回答这个问题,让我们看另一个示例-内置的.sort()数组方法。 是的,他有缺点。 它更改数组而不是返回新数组。 但是,让我们暂时忘记它。 .sort()方法是一个高阶函数;它采用另一个函数作为参数之一。

如何运作? 如果要对数字数组进行排序,则首先需要创建一个比较函数:

 function compareNumbers(a, b) { if (a === b) return 0; if (a > b) return 1; /* else */ return -1; } 

现在对数组进行排序:

 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'], } ]; 

假设我想按每个记录的权重对该数组进行排序。 我可以从头开始编写新的排序函数。 但是为什么,如果您可以创建一个新的比较功能:

 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年中,聪明的程序员从那里借来了很多想法,这些想法已变成语言和库的属性。 如果我们学习了这些模式,那么有时我们可以替换大段代码。 或将复杂的问题简化为简单的构建块的优雅组合。 这些块是高阶函数。 因此,它们是如此重要,它们为我们提供了一个强大的工具来应对代码的复杂性。

有关高阶函数的其他材料:


您可能已经在使用高阶函数。 这在JavaScript中是如此简单,以至于我们甚至都没有考虑过。 但是最好知道人们说这句话时在说什么。 这并不困难。 但是,一个简单的想法背后蕴藏着巨大的力量。

如果您有函数式编程方面的经验,您可能会注意到我使用的不是纯函数,而是一些...详细的函数名。 这不是因为我没有听说过不干净的函数或函数编程的一般原理。 而且我不会在生产中编写此类代码。 我试图挑选一些对于初学者来说很清楚的实例。 有时我不得不妥协。 如果有兴趣,我已经写过关于函数清洁度函数式编程一般原理的文章

Source: https://habr.com/ru/post/zh-CN458896/


All Articles