高阶函数(回调函数)

325 阅读13分钟

​ 今天我们来学习一个知识点:高阶函数。我们不要被高阶函数这个名词吓唬到了,高阶函数不过就是一个定义的函数的概念。我们只需要知道高阶函数代表什么含义就ok了。

​ 我们需要知道的是,在JavaScript中,函数的一等公民。可见函数特别特别重要。因为在JavaScript中,函数特别的灵活。

1、函数既可以作为一个变量值;

2、函数又可以作为函数的参数;

3、函数还可以作为函数的返回值。

而满足第二点和第三点的函数,就是高阶函数。

什么是高阶函数?

函数作为参数或者函数作为返回值的时候,那么就是高阶函数。

# 1、函数作为'参数'
# 2、函数作为'返回值'

一、函数作为参数

那么为什么需要让函数作为参数

​ 为了方便理解,我们不妨举一个栗子。

我们每天都要吃晚饭。吃完晚饭之后,我们去做什么事情就非常多选择了。你可以打游戏,你可以学习,你可以去跳广场舞。你可以去浪等等等等。每一天的饭后活动都可以不一样。

# 代码编写

​ 那么我们如何通过代码的方式来模拟上面这种场景呢?

我们可以将吃晚饭定义函数。

Snipaste_2020-09-15_06-40-33

接着模拟一个吃晚饭的动作。假设我们吃两秒钟。

Snipaste_2020-09-15_06-52-45

我们调用吃饭的方法挺简单的。如下所示:

Snipaste_2020-09-15_07-18-07

我们打印出来看一下,这就算吃完晚饭了。

Snipaste_2020-09-15_07-20-31

那么接下来,我们并不知道吃完晚饭之后,要做什么事情是不确定的。

Snipaste_2020-09-15_09-19-29

​ 既然是不清楚吃晚饭以后该做些什么,因此是不固定的,不固定的话,我们就不能直接将事件写死在代码中。我们应该将,饭后的活动暴露出去,让调用者自己去决定做些什么?

​ 概念理解起来很简单,但是应该如何体现在代码中呢?

这个就需要我们用到**“函数作为参数”**的知识点了。具体操作如下:

Snipaste_2020-09-15_09-22-35

然后我们定义吃完饭后的事件。并不需要直接写死,而是使用代指的形式编写。

Snipaste_2020-09-15_09-38-40

这样就算已经编写好了,我们并没有直接将饭后的活动直接写死。将具体的活动暴露出去了。

接下来,我们开始编写具体饭后的活动。假设我们饭后去学习。那么我们该怎么做呢?

Snipaste_2020-09-15_09-42-44

具体的编码如下。

Snipaste_2020-09-16_09-03-09

其实这样就算编写完成了,我们打印出看一下。

Snipaste_2020-09-16_09-07-07

# 可以看出,出完晚饭之后,就去学习。

其实上面的代码就算编写完成了,最终的代码如下:

Snipaste_2020-09-16_09-10-29

# 执行过程

如果说我们想要看上面的代码如何工作,也就是代码执行的过程,我们可以通过断点的方式进行调试。

Snipaste_2020-09-16_11-38-35

接着在你想要的测试的位置上,打上一个断点。

首先我们需要知道的是,下面这段函数是不会自己执行的,因此没有必要打上断点。

Snipaste_2020-09-16_11-52-21

我们需要在调用的位置上打上断点。

Snipaste_2020-09-18_09-22-20

然后按住F5刷新一下,进入断点调试状态。

Snipaste_2020-09-18_09-45-23

按住F11进入函数执行setTimeout函数

Snipaste_2020-09-18_09-47-33

然后在按F11往下执行,你会发现会停留两秒钟后,会执行下面这段代码。

Snipaste_2020-09-18_09-47-51

# 其实这也说明了,只是进入了setTimeout函数内部执行。其实你细心的话,你会发现,其实setTimeout函数也是一个以函数作为参数的方法。此时执行的是setTimeout的参数中的函数。

其实你细心的话,你会发现,其实在setTimeout函数中,其实也是用了高阶函数(函数作为参数)的形式在编写代码。

Snipaste_2020-09-15_08-16-0323

接着执行fn()

Snipaste_2020-09-18_09-48-12

紧接着会进入eat函数内的参数中,执行参数中的代码段。

Snipaste_2020-09-18_09-48-27

# 这一段就是执行过程中最为重要的地方,我们在吃完饭之后,接着就会执行我们定义在外面的活动。

直到eat函数参数内的函数执行完毕

Snipaste_2020-09-18_09-48-41

然后会再次跳会setTimeout函数内执行。这个时候setTimeout函数也执行完毕了。

Snipaste_2020-09-18_09-48-58

# 可以看出,执行完活动之后,又会回到setTimeout函数中执行剩余的代码。因为这里并没有剩余的代码了,因此才会结束掉setTimeout函数。然后再结束掉eat函数。这样整个代码就算执行完毕了。

其实这个时候就算执行好了整个过程。

Snipaste_2020-09-18_09-50-15

# 了解回调函数的执行过程是非常有必要的。这样的话,我们在编写代码的时候才能心中有数。

案例1:myforEach的封装

在遍历数组的时候,有没有想过有了for循环的情况下,为什么官方还提供forEach方法?

​ 要知道在forEach方法没有出来之前,我们可以使用基于原生JS中的循环(就是使用for循环)就可以实现遍历数组中每一项内容。如下所示:

let ary = [12, 15, 9, 28, 10, 22];

for(let i=0;i<ary.length;i++){
    // i:代表的是当前循环的索引。
    // ary[i]:根据索引获取循环的某一项值。
    console.log('索引:'+i+'内容:'+ary[i]);
}

我们把上面这段代码编写在编辑器中。

Snipaste_2020-08-27_08-24-43

然后运行一下看看,显示的结果。

Snipaste_2020-08-27_08-26-30

# 我们可以看出,通过普通的for循环,就能够实现遍历数组中每一项内容的功能。

​ 但是除了使用for循环以外,我们还可以通过另一种方式来编写。也就是使用forEach方法来进行遍历数组中的元素。代码如下所示:

let ary = [12,15,9,28,10,22];
ary.forEach(
    (item,index)=>{
		console.log('索引:'+index+'内容:'+item);
    }
)

我们把上面这段代码编写在编辑器中。 Snipaste_2020-08-27_10-01-20

# 运行之后,你就可以发现使用forEach的方法就是实现相同的效果。

那么这个时候,你有没有想过,forEach方法里面是如何实现数组的遍历的呢?

​ 其实答案很简单,其实forEach方法的本质还是for循环。只不过for循环被封装成了回调函数for循环forEach方法本质上是一样的,只是在外界使用的方式上看上去显的不一样。

那么为什么还要多此一举的将for循环封装成forEach方法呢?

Snipaste_2020-09-09_12-47-52

​ 其实我们单纯从代码上进行比较的话,就能看出来,在使用forEach方法的时候,要比for循环要好用很多。在使用forEach方法的时候,我们并不需要考虑“i++”这类的递增关系。只需要关注itemindex。就是完成遍历的操作。因此通过封装的代码,会让代码变得更加的方便的使用。日后的代码维护也变得方便很多。这就是为什么需要大费周章过的对for循环进行封装的原因。

如何证明forEach方法的本质就是for循环呢?

​ 最直接的方法,自然是直接查看源码。这里我就不介绍了,我们来个更好玩的,我们试试用回调函数的方法来封装一个属于自己的forEach方法。实现和forEach方法相同的效果。

具体封装过程:

我们可以看出这个通过for循环遍历的话,代码是这样的。

Snipaste_2020-09-11_09-35-14

而forEach方法,本质上就是对for循环的封装。而封装的方式是通过回调函数进行封装的。

# 回调函数:编写在方法中的函数。

因此首先我们需要编写一个方法用来管理for循环。

Snipaste_2020-09-11_09-44-39

然后,我们需要在方法中添加fn参数。其中fn代指的是方法。

Snipaste_2020-09-11_10-06-28

接下来,我们就要考虑fn函数的编写情况了。我们需要考虑的是,需要遍历的数据是什么?

Snipaste_2020-09-11_10-26-39

但是这会有个问题是,我们不能直接使用ary,因为这样的话会限制我们封装的代码。

Snipaste_2020-09-11_10-44-41

我们应该将数组的名称决定权交给调用者。我们可以用this来指代需要遍历的数组名称。

Snipaste_2020-09-11_11-19-44

接下来我们重点看一下如何编写fn函数。其实就是对打印出来的结果进行改造。

Snipaste_2020-09-11_11-31-57

# 其实这样就算封装完毕了,是不是很简单。

如果是在前端使用的话,我们还需要将自己定义的myForEach方法定义到Array.peototype中。在微信小程序中使用自己定义的myForEach方法的话。只需要引入就可以使用。没有这么麻烦。

Snipaste_2020-09-11_11-51-38

打印出来看下结果。

Snipaste_2020-09-11_11-59-24

当传递进去之后,那么我们在后续遍历数组的时候,myForEach方法才会起到作用。这个时候我们试着使用起来。

Snipaste_2020-09-11_12-10-13

为什么需要使用这么奇怪的调用方法结构。ary.myForEach()

其实原因在于我们在编写myForEach方法的时候,为了能够封装一个通用性较强的方法,我们用了this来替代数组名称。(前面已经说过)。既然在封装的时候没有规定是哪一个特定的数据,那么我们在使用的时候,就需要先告诉一下myForEach方法需要使用的具体数组名,然后再使用方法。也就是这样的ary.myForEach()结构。

那么这个时候,我们需要如何使用myForEach方法呢?

Snipaste_2020-09-11_12-34-20

这个你需要知道的是,index参数内部存储的就是for循环中的索引i,item参数内部存储的是for循环中的

Snipaste_2020-09-11_12-41-29

然后我们就可以执行遍历数组了。

Snipaste_2020-09-11_13-11-50

打印出来看一下效果

Snipaste_2020-09-11_13-09-42

# 这就把forEach方法的效果给模拟出来了。可以看出forEach方法本质就是for循环。只不过我们通过回调函数的方式将for循环进行了封装。

细心的你会发现,其实myforEach方法的使用方式和数组内置的forEach方法的使用方法是一样的。

Snipaste_2020-09-11_13-19-01

我们还可以完善一下代码,如下所示:

Snipaste_2020-09-08_13-49-18

最终打印出来的结果如下:

Snipaste_2020-09-08_13-51-58

可以看出,本质上还是通过回调函数的形式进行来封装。


上篇的案例来源于:www.jianshu.com/p/a07975ae2…

案例2:双数组的封装

和封装myForEach方法的原理一样,我们可以封装一下双数组。前提在于我们需要对双数组足够的了解,封装的代码如下:

Snipaste_2020-09-11_13-58-42

运行看一下:

Snipaste_2020-09-11_13-59-57

案例3:express 回调函数

​ 要知道,现在是大前端的时代,JavaScript已经不再是哪个编写一些跑马灯的年代了,自从有了Node以后,JavaScript已经可以触及后端的领域。这些年里,涌现出很多的框架,比如express和Koa。而这些代码中,会经常使用到回调函数,满眼看上去,到处都是以函数作为参数的回调函数的使用。如果对回调函数理解不了的话,那么对应代码的理解上会变得非常的吃力。

比如下面这段代码,我们进行分析一下。

app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

app是对象,use是方法。

Snipaste_2020-09-08_11-53-36

use方法的参数,是一个带有参数的匿名函数。

Snipaste_2020-09-08_12-00-25

而最里面这块是函数体。

Snipaste_2020-09-08_12-05-03

​ 其实你会发现上面这一块代码也是运用了回调函数的知识点。要知道,在使用nodejs、express 的时候,不可能每个函数我们都要找到它的函数定义去看一看。所以只要知道那个定义里面给 callback 传递了什么参数就行了。然后在调用函数时,在参数里我们自己定义匿名函数来完成某些功能。

二、函数作为返回值

参考教程:

https://www.bilibili.com/video/BV1WE411t7kL?p=57

待完善