【译】函数式 JavaScript:你们所关心的高阶函数

原文:FUNCTIONAL JAVASCRIPT: WHAT ARE HIGHER-ORDER FUNCTIONS, AND WHY SHOULD ANYONE CARE?javascript

声明:翻译原文从国外知名博客网站上获取,并利用业余时间进行翻译。若是发现网络上有其余译文,多是由于开始翻译时没有发现已存在译文或是感受原译文翻译质量不佳而从新翻译。不论出于哪类缘由,本译文不会包含任何抄袭行为。html


译者序:

当前,面向对象语言盛行,不少人以为函数式编程只存在于一些偏门语言中,并在特定的需求下使用。如今不少语言都引入了函数式编程的特性,并吸纳其优势,如咱们最熟悉的 JDK,JSDK8 已经引入了函数式编程的一些特性。而对于前端开发者而言,函数式编程看似遥远,其实很近。前端

JavaScript 自然支持高阶函数和闭包,其实已经让函数式编程融入到平时的工做中。哪怕没听过函数式编程的人,也都使用过函数式编程的方式。java

广义地说,全部 Callback 类的调用,例如 DOM 件的监听、数组方法(forEach、Map)等的使用,都属于函数式编程的范畴。react

这篇文章,立足于 JavaScript 中的函数,为你们剖析函数式编程里最重要的高阶函数,让读者能够对 JavaScript 中的函数式编程有必定的了解。程序员

正文

“高阶函数”是人们抛出的一个概念,可是你们很难解释清楚它意味着什么?也许你已经知道什么是高阶函数,可是你并不清楚如何在现实中使用?什么状况下使用?使用后产生什么效果?甚至说,使用了高阶函数之后,获得了什么好处?是否值得炫耀?反过来,是否会由于烂用它们形成代码复杂度上升?算法

我我的刚好认为高阶函数是很是有用的,而事实上,我认为它们是 JavaScript 做为一种语言最重要的特性之一,而上面的问题,将在文中一一解答。编程

但在开始以前,让咱们先来深刻分析一下高阶函数。 为此,文章将从“把函数赋值给变量”开始。redux

函数做为“一等公民”

在 Javascript 中,咱们至少有三种方式编写一个函数。首先,能够编写一个函数声明,示例以下。数组

// 拿到一个 Dom 对象,并放在 li 节点里。
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(),咱们会获得一个 NodeList 而不是一个数组。NodeList 没有像数组那样的 .map() 方法,因此写一个:

// 将给定函数应用于 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' 类的全部 button。
const loadButtons = document.querySelectorAll('button.loader');
​
// 将 spinner 类添加到咱们找到的全部 button 上。
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) {
    // 直接返回一个函数。
    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 元素。
复制代码

返回的函数“记住”某些内容具备技术名称的业务,这称之为封闭。 封闭过于方便,但如今不会过多担忧它们。

因此,咱们已经看到:

  • 为变量分配函数;
  • 将函数做为参数传递;
  • 从另外一个函数返回一个函数。

总而言之,拥有这些高级的功能是至关不错的。但这与高阶函数有什么关系呢? 下面让咱们看看高阶函数的定义。

高阶函数是什么?

高阶函数是:

A function that takes a function as an argument, or returns a function as a result(将函数做为参数的函数,或做为结果返回函数的函数)

听起来有点耳熟? 在 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) {
        // 注意,== 是故意的。
        if ((args.length === 0) || args.some(a => (a == null)) {
            return undefined;
        }
        return fn.apply(this, args);
    }
}
复制代码

如今先看看如何使用它,而不是解释它如何工做。 再次查看函数 elListMap()

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

复制代码

若是将 null 或未定义的值传递给 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年左右,聪明的程序员一直在借鉴它们的想法,这些想法主要表现为编程语言功能和库。若是学习这些模式的模式,有时候能够删除整个代码,或者将复杂问题简化为多个简单构建块的优雅组合。这些构建块就是高阶函数。上面所说就是高阶函数很重要的缘由,由于有了它们,就有用了一个能对抗代码中复杂性的强大工具。

结语

若是你想了解有关高阶函数的更多信息,请参考如下内容:

由于 JavaScript 已经支持了高阶函数,避免了考虑使用方式的问题,让咱们能够很容易使用高阶函数的方式去实现、优化一些功能。而你们在了解这些以后,会发现高阶函数并不复杂,它很方便地帮咱们去完成一些事情。

可是,在这个看似简单的高阶函数背后,包含着函数式编程的思想、理论和范式。当你步入这个领域,你会发现它如此强大。

相关文章
相关标签/搜索