Javascript 函数式编程:什么是高阶函数?咱们为何须要了解它?

原文:jrsinclair.com/articles/20…
做者:James Sinclair
翻译:前端小白javascript

高阶函数是你们常常挂在嘴边的,可是不多有人去解释它是什么,也许你已经知道什么是高阶函数了。可是咱们如何在现中使用它们?有哪些例子能够告诉咱们何时使用,以及他们怎么表现出实用性?咱们可使用他们操做DOM?或者,那些使用高阶函数的人实在炫耀吗?他们将代码过度复杂化?html

我认为高阶函数颇有用。事实上,我认为它们是JavaScript做为一种语言最重要的特性之一,但在讲这个以前,咱们先来分解一下什么是高阶函数,在理解这个概念以前,让咱们从函数做为变量开始提及。前端

函数是头等公民

在JavaScript中,咱们至少有三种不一样的方法来编写函数。首先,咱们能够写一个函数声明。例如:java

// 接受一个DOM元素,将他包裹在li里面
function itemise(el) {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}
复制代码

但愿你们都很熟悉。可是,你可能知道咱们也能够把它写成函数表达式。看起来是这样的:react

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(),咱们将返回 NodeList,而不是一个数组。NodeList 没有像数组那样的 .map() 方法,因此咱们来写一个:redux

// 将给定的函数应用于NodeList中的每一项并返回一个数组。
function elListMap(transform, list) {
    // list 多是一个 NodeList, 没有 .map() 方法, 因此咱们将它转换为一个数组
    return [...list].map(transform);
}

// 获取页面中全部类名为 'for-listing' 的 span 标签.
const mySpans = document.querySelectorAll('span.for-listing');

// 将每项包裹在 li 里面. 咱们使用以前定义的 itemise 函数
const wrappedList = elListMap(itemise, mySpans);
复制代码

在这个例子中,咱们将 itemise 函数做为参数传递给 elListMap 函数,可是咱们可使用 elListMap 不只仅是建立列表,例如,咱们可使用它向一组元素添加一个类。数组

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

// 获取全部类名为 'loader' 的按钮
const loadButtons = document.querySelectorAll('button.loader');

// 将 spinner 类名添加给全部的 button
elListMap(addSpinnerClass, loadButtons);
复制代码

elListMap 函数接受 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) {
    // 很直接,咱们返回了一个函数
    return function wrap(children) {
      // 在wrap函数里面, 咱们能够‘看到’ elementType 这个参数
      const parent = document.createElement(elementType);
      return [...children].reduce((parentEl, child) => {
          parentEl.appendChild(child);
          return parentEl;
      }, parent);
    }
}
复制代码

刚开始看起来有点复杂,咱们把它分解一下。咱们建立了一个函数,它没有其余功能,只是返回另外一个函数。可是这个被返回的函数记住了 elementType 这个参数,那么,当咱们随后调用这个被返回的函数时,它就知道该建立什么元素,因此咱们能够建立 wrapWithUlwrapWithDiv

const wrapWithUl  = createListWrapperFunction('ul');
// wrapWithUl() 函数如今记住了,它要建立一个 ul 元素

const wrapWithDiv = createListWreapperFunction('div');
// wrapWithDiv() 函数如今记住了,它要建立一个 div 元素
复制代码

返回的函数会“记住”某个东西,咱们有个专业的叫法:闭包。闭包很是实用,可是咱们如今还不用想太多。 因此,咱们已经看到了:

  • 将函数赋值给变量
  • 做为参数传递
  • 从一个函数返回另外一个函数

总而言之,函数是头等公民,这确实不错。但这和高阶函数有什么关系呢?咱们来看看高阶函数的定义。

什么是高阶函数

高阶函数是:

将函数做为参数传入或做为结果返回的函数

听起来是否是很熟悉?在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的 JavaScript Allongé 一书中改写了这个函数,看起来像这样:

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() 函数:

function elListMap(transform, list) {
    return [...list].map(transform);
}
复制代码

若是咱们不当心将一个 nullundefined 值传递给 elListMap(),会发生什么?咱们会获得一个 TypeError,程序会崩溃,中止执行。maybe() 解决了这个问题,咱们能够这样使用:

const safeElListMap = maybe(elListMap);
safeElListMap(x => x, null);
// ← undefined
复制代码

这时候函数会返回 undefined,而不是崩溃。若是咱们把它传递给另外一个受 maybe() 保护的函数,它会再次返回 undefined。咱们可使用 maybe() 函数 来保护咱们任何咱们想要的函数。这比用 if 语句要简单得多。

一个函数返回另外一个函数,在React社区中也很常见。例如,react-redux 中的 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年左右,聪明的程序员一直在窃取他们的想法,这些思想表现为编程语言特性和库。

若是咱们学习这些模式的模式,咱们有时能够移除大片代码。或者将复杂的问题分解为简单构建快之间的组合,那些构建快就是高阶函数,这就是为何高阶函数很重要。由于有了它们,咱们就有了一个强大的工具来对抗代码中的复杂性。

若是你想了解更多关于高阶函数的知识,这里有一些参考资料:

Higher-Order Functions Chapter 5 of Eloquent JavaScript by Marijn Haverbeke.
Higher-Order Functions Part of the Composing Sofware series by Eric Elliott.
Higher-Order Functions in JavaScript by M. David Green for Sitepoint.

可能你已经在使用高阶函数了。JavaScript使它变得如此简单,以致于咱们没有过多地考虑它们。可是当人们抛出这个词时,咱们知道这是什么,这并不复杂。但在这看似简单的概念背后,蕴藏着巨大的力量。

Update 3 July 2019:若是你是一名有经验的函数式编程开发者,可能你已经注意到我使用了非纯函数和一些冗长的函数名。这并非由于我不了解非纯函数或通常函数编程原理。这不是我在生产环境中定义函数名的方式。这是一篇有教育意义的文章,因此我尽可能选择一些初学者能理解的实际例子,做为一种妥协。若是你有兴趣,能够看看我另外两篇文章 functional puritygeneral functional programming principles

最后

  1. 函数有三种以上的写法,不过咱们能够下次再讨论。
  2. 这并不老是正确的。这三种写法在实践中都有细微的差异。区别在于 this 关键字和函数调用堆栈跟踪过程当中标签的变化
  3. 维基百科:Wikipedia contributors (2019). ‘First–class citizen,’ Wikipedia, the free encyclopedia, viewed 19 June 2019, en.wikipedia.org/wiki/First-…
  4. 若是你想了解更多关于闭包,参考: Master the JavaScript Interview: What is a Closure? by Eric Elliott
  5. Higher Order Function (2014), viewed 19 June 2019, wiki.c2.com/?HigherOrde….
相关文章
相关标签/搜索