原文: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
这个参数,那么,当咱们随后调用这个被返回的函数时,它就知道该建立什么元素,因此咱们能够建立 wrapWithUl
和 wrapWithDiv
:
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);
}
复制代码
若是咱们不当心将一个 null
或 undefined
值传递给 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 purity 和 general functional programming principles
this
关键字和函数调用堆栈跟踪过程当中标签的变化