深入了解回调函数和回调地狱 | 青训营笔记

97 阅读7分钟

笔记创作活动的的第7天


认识JS中的函数对象

在JS中万物皆为对象,由于函数的特殊性,函数在JS解析和执行时,会被维护成一个对象,这就是要介绍的函数对象

  • 我们可以使用function关键字定义一个函数,并为每个函数指定一个函数名,通过函数名来进行调用。
 //函数声明的三种方式
 ​
 //第一种:普通方式声明(常用)
 function f1(num1,num2){
     return num1 + num2
 }
 ​
 //第二种:使用变量初始化的方式声明
 var f2 = function(num1,num2){
     return num1 + num2
 }
 ​
 //第三种:使用Function构造函数的方式(微信小程序不允许使用这种方法,new Function('return this')除外)
 var f3 = new Function('num1','num2','return num1 + num2')
复制代码
  • 函数是用Function()构造函数创建的Function对象。函数对象(Function)与日期对象(Date)、数组对象(Array)、字符串对象(String)等都称为内部对象,我们可以通过typeof( )函数查看一个对象的类型。
 console.log(typeof (Function));  //function
 console.log(typeof (Array));  //function
 console.log(typeof (Object));  //function
 console.log(typeof (new Array()));  //object
 console.log(typeof (new Date()));  //object
 console.log(typeof (new Object()));  //object
 console.log(typeof (new Function()));  //function //小程序不能运行这行代码
 console.log(new Function() instanceof Object);    //true
 ​
 //由此可见,对象是通过函数创建的,而函数本身又是一个类型为Function的特殊对象。
 //Function是所有函数对象的基础,而Object则是所有对象(包括函数对象)的基础。
复制代码

回调函数

经过上一节的了解,在JS中函数也是一类对象,这意味着函数能够像对象一样被使用。既然函数实际上是对象:它们能被“存储”在变量中,能作为函数参数被传递,能在函数中被创建,能从函数中返回。

回调函数是从一个叫函数式编程的编程范式中衍生出来的概念。简单来说,函数式编程就是使用函数作为变量。函数式编程中的一个主要技巧就是回调函数。

简单来说回调函数是一个被作为参数传递给另一个函数的函数。

举个例子:你到一个商店去购物,刚好你想要的东西没有货,于是你在店员那里留下了你的电话,叫店员有货了就通知你,过了几天店里有货了,店员于是打电话联系你,然后你接到电话后就去店里取了货。在这个例子里面,你将号码留下并让店员在有货时通知你的这个行为就叫回调函数,店里后来有货了叫做触发了回调关联的事件,店员打电话通知你叫做调用回调函数,你到店里去取货叫做响应回调事件

1、常见的回调函数

(1)DOM事件回调

 document.getElementById('btn').onclick = function(){
     alert('hello world!')
 }
复制代码

(2)定时器回调

 setTimeout(function(){
     alert('hello world!')
 },1000)
复制代码

2、回调函数的特点

(1)将回调函数作为变量传递给触发函数,体现了变量的灵活性。可将通用的逻辑抽象,将回调函数作为专职的函数进行分离,提高代码的可维护性和可读性。

 //方式一
 function a(){
     console.log('a函数原本执行的内容');
     b() //直接在a函数内调用b函数
 }
 //这样子也能到达由a函数控制b函数执行的目的,但是将b函数写在a函数内就会被限制住了,失去了灵活性
 ​
 //方式二
 function a(callback){
     console.log('a函数原本执行的内容');
     callback();
 }
 function test(){ //这样子可以灵活地去在a函数执行时触发不同的回调函数
     a(b);
     a(c);
 }
 ​
 //两个回调函数
 function b(){
     alert("我是回调函数b");
 }
 function c(){
     alert("我是回调函数c");
 }
 ​
复制代码

(2)回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。因此,回调本质上是一种设计模式。

(3)不会立即执行,回调就是一个函数被另一个函数调用的过程。例如:函数a有一个参数,这个参数是函数b,函数a会决定函数b在什么时候被执行。那么这个过程就叫回调,即函数b在某个特定的时间点被函数a回调(通常这个特定的时间点是在函数a中的异步任务执行完后)。

(4)回调函数的闭包,我们将一个回调函数作为变量传递给另一个函数时,这个回调函数在包含它的函数内的某一特定时间点执行,就好像这个回调函数是在包含它的函数中定义的一样。这意味着回调函数本质上是一个闭包。正如我们所知,闭包能够进入包含它的函数的作用域,因此回调函数能获取包含它的函数中的变量,以及全局作用域中的变量。

3、回调函数的使用

(1)使用命名或匿名的函数作为回调

 function a(callback){
     console.log('a函数执行了')
     callback()
 }
 ​
 function b(){
     console.log('回调函数b执行了')
 }
 ​
 function test(){
     a(b) //使用命名的回调函数
     a(function(){ //使用匿名的回调函数
         console.log('匿名回调函数函数执行了')
     })
 }
复制代码

(2)传递参数给回调函数

 var name = 'dj'
 ​
 function a(name,callback){
     console.log('a函数执行了')
     callback(name)
 }
 ​
 function b(name){ //回调函数b需要一个参数
     console.log('回调函数b执行了')
     console.log('hello '+name)
 }
 ​
 function test(){
     a(name,b) //这里的第一个参数name就是第一行代码定义的name变量,在此时传入给到回调函数b使用;第二个参数就是回调函数b
 }
复制代码

(3)在执行前确保回调函数是一个函数,如果不确定传入的callback是否为一个函数,推荐用这种写法,先判定是否为函数再进行调用,以免出现不必要的报错。

 function a(callback){
     console.log('a函数执行了')
     //确保callback是一个函数   
     if(typeof callback === "function"){
         callback();
     }
 }
 ​
 function b(){
     console.log('回调函数b执行了')
 }
 ​
 function test(){
     a(b)
 }
复制代码

回调地狱

1、同步任务与异步任务

同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行下一个任务。异步任务不进入主线程,而是进入异步队列,前一个任务是否执行完毕不影响下一个任务的执行。

 //同步
 console.log('1')
 console.log('2')
 console.log('3')
 //输出结果为 1 2 3
 ​
 //异步(以定时器为例)
 setTimeout(function(){
     console.log('1');
 },1000)
 console.log('2')
 //如果按照代码的编写顺序,应该先输出1再输出2,但实际输出为2 1
复制代码

像定时器这种不阻塞后面代码执行的任务就叫异步任务。

2、回调地狱是什么?

根据上文我们可以得出一个结论:存在异步任务的代码,不能够确保按照顺序执行。

如果我们想要在异步任务里按顺序执行一段代码,必须要如下操作才能保证顺序正确:

 //假设我们想要按顺序分开输出一段文字:面向对象面向君,不负Java不负卿
 setTimeout(function () { //第一层
     console.log('面向对象');
     setTimeout(function () { //第二程
         console.log('面向君');
         setTimeout(function () { //第三层
             console.log('不负Java');
              setTimeout(function () { //第四层
                  console.log('不负卿');
              }, 500)
         }, 1000)
     }, 2000)
 }, 3000)
 ​
 //假设业务开发中有三个接口,每个接口都依赖于前一个接口的返回值,就会出现以下情况
 request({ //第一次请求
   url: 'url1',
   data: 'data1',
   success(res1) {
     request({ //第二次请求
       url: 'url2',
       data: res1, //依赖于第一个接口的返回值res1
       success(res2) {
         request({ //第三次请求
           url: 'url3',
           data: res2, //依赖于第二个接口的返回值res2
           success(res3) {
             console.log(res3)
           }
         })
       }
     })
   }
 })
 ​
 //可以看到代码中的回调函数嵌套回调函数,这种情况就称为回调地狱
复制代码

简而言之,回调地狱就是在异步JS里面过多的使用了回调函数,回调函数层层嵌套,使得代码可读性差,且后期不易于维护。

后续的文章还将介绍如何解决回调地狱的问题,欢迎大家关注。