【JavaScript】【回调】回调函数 && 回调地狱

前言

首先,我们先思考下什么是回调函数?什么是回调地狱?

一、回调函数

首先,我们需要清楚一点:JavaScript 函数按照他们被调用的顺序执行,而不是以它们被定义的顺畅

1.1 什么是回调函数

回调是作为参数传递给另一个函数的函数。并且可以在传入的那个函数中接受参数和返回值

回调函数和普通函数没有本质区别,没有本质区别,回调函数谁都可以用

小小的举个栗子:

js 复制代码
function myDisplayer(some) {
  document.getElementById("demo").innerHTML = some;
}

// myCallback 就是回调函数的传参,我们还可以给回调函数传参
function myCalculator(num1, num2, myCallback) {
  let sum = num1 + num2;
  myCallback(sum);
}

myCalculator(5, 5, myDisplayer);

【注意点】:

  • 函数作为参数传递时,不要使用括号
    • 正确: myCalculator(5, 5, myDisplayer);
    • 错误:myCalculator(5, 5, myDisplayer());

1.2 回调函数的作用和使用场景

回调函数是一种常见的编程技术,他可以在异步操作完成后调用一个预定义的函数来处理结果。回调函数通常用于处理事件、执行异步操作或响应用户输入等场景。

  • 作用:
    • 代码逻辑分离出来,使得代码更加模块化和可维护
    • 可以避免阻塞程序的运行,提高程序的性能和效率
    • 可以实现代码的复用,因为他们可以被多个地方调用
  • 使用场景:
    • 事件处理:例如鼠标点击、键盘输入,网络请求等
    • 异步操作:例如读取文件、发送邮件、下载文件等
    • 数据处理:例如对数组进行排序、过滤、映射等
    • 插件开发:例如WordPress插件等
  • 异步编程:指在代码执行时不会阻塞程序运行的方式
  • 事件驱动:指程序的执行是由外部事件触发而不是顺序执行的方式

1.3 回调函数的优缺点

  • 优点
    • 提高代码的复用性和灵活性:回调函数可以将一个函数作为参数传递给另一个函数,从而实现模块化编程,提高代码的复用性和灵活性
    • 解耦合:回调函数可以将不同模块之间的关系解耦,使得代码更易于维护和扩展
    • 可以异步执行:回调函数可以在异步操作完成后被执行,这样避免了阻塞线程,提高应用程序的效率
  • 缺点
    • 回调函数嵌套过多会导致代码难以维护:如果回调函数嵌套层数过多,代码会变得非常复杂,难以维护
    • 回调函数容易造成竞态条件:如果回调函数中有共享资源访问,容易出现竞态条件,导致程序出错
    • 代码可读性差:回调函数的使用可能会破坏代码的结构和可读性,尤其是在处理大量数据时

小结:代码灵活、易于扩展,但是不易于阅读、容易出错

1.4 回调函数与其它编程概念的关系

1.4.1 回调函数和闭包的关系

回调函数和闭包之间存在着紧密的关系。回调函数是一个函数,在另一个函数中被作为参数传递,并在该函数执行完成后被调用。闭包是由一个函数及其相关的引用环境组合而成的实体,可以访问函数外部的变量

在某些情况下,回调函数需要访问到它所在的父函数的变量,这时就需要使用闭包来实现。通过将回调函数放在闭包内部,可以将父函数的变量保存在闭包的引用环境中,使得回调函数能够访问到这些变量。同时,闭包还可以保证父函数中的变量在回调函数执行时不会被销毁,从而确保了回调函数的正确性

因此,回调函数和闭包是一对密切相关的概念,常常一起使用来实现复杂的逻辑和功能。

1.4.2 回调函数和Promise的关系

C++回调函数和Promise都是异步编程的实现方式。

回调函数是一种将函数作为参数传递给另一个函数,在异步操作完成后执行的技术。在C++中,回调函数通常使用函数指针或函数对象来实现。当异步操作完成后,会调用注册的回调函数,以便执行相应的处理逻辑。

而Promise则是一种更加高级的异步编程模式,它通过解决回调地狱问题,提供了更加优雅和简洁的异步编程方式。Promise可以将异步操作封装成一个Promise对象,并通过链式调用then()方法来注册回调函数,以及catch()方法来捕获异常。当异步操作完成后,Promise会自动根据操作结果触发相应的回调函数。

因此,可以说C++回调函数和Promise都是异步编程的实现方式,但是Promise提供了更加高级和优雅的编程模式,能够更好地管理异步操作和避免回调地狱问题。

1.4.3 回调函数和观察者模式的关系

回调函数和观察者模式都是用于实现事件驱动编程的技术。它们之间的关系是,观察者模式是一种设计模式,它通过定义一种一对多的依赖关系,使得一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。而回调函数则是一种编程技术,它允许将一个函数作为参数传递给另一个函数,在执行过程中调用这个函数来完成特定的任务。

在观察者模式中,当一个被观察的对象发生改变时,会遍历所有的观察者对象,调用其定义好的更新方法,以进行相应的操作。这里的更新方法就可以看做是回调函数,因为它是由被观察对象调用的,并且在执行过程中可能需要使用到一些外部参数或上下文信息。因此,可以说观察者模式本身就包含了回调函数的概念,并且借助回调函数来实现观察者模式的具体功能。

1.5 如何编写高质量的回调函数

回调函数需要遵循以下几个原则:

  1. 明确函数的目的和作用域。回调函数应该有一个清晰的目的,同时只关注与其作用范围相关的任务
  2. 确定回调函数的参数和返回值。在定义回调函数时,需要明确它所需的参数和返回值类型,这样可以使调用方更容易使用
  3. 谨慎处理错误和异常。回调函数可能会引发一些异常或错误,需要使用 try-catch 块来处理它
  4. 确保回调函数不会导致死锁或阻塞。回调函数需要尽可能快地执行完毕,以避免影响程序的性能和稳定性。
  5. 使用清晰且易于理解的命名规则。回调函数的命名应该清晰、简洁,并尽可能说明其功能和意义。
  6. 编写文档和示例代码。良好的文档和示例代码可以帮助其他开发者更容易地使用回调函数,同时也有助于提高代码的可维护性和可重用性。
  7. 遵循编码规范和最佳实践。 编写高质量的回调函数需要遵守编码规范和最佳实践,例如使用合适的命名规则、注释代码等。

1.5.1 回调函数的命名规范

回调函数的命名规范没有固定的标准,但是根据通用惯例和编码规范,回调函数的命名应该能够反映函数的作用和功能,让其他开发者能够快速理解并使用。

  1. 使用动词+名词的方式来描述回调函数的作用,例如onSuccess、onError等。
  2. 如果回调函数是用于处理事件的,可以以handleEvent或者onEvent作为函数名。
  3. 如果回调函数是用于处理异步操作完成后的结果,可以以onComplete或者onResult作为函数名。
  4. 在命名时要注意保持简洁明了,不要过于冗长,也不要使用缩写或者不清晰的缩写。
  5. 尽量使用有意义的单词或者短语作为函数名,不要使用无意义的字母或数字组合。
  6. 与代码中其他的函数名称保持一致,尽量避免出现命名冲突的情况。

1.5.2 回调函数的参数设计

回调函数的参数设计取决于回调函数所需执行的操作和数据。一般来说,回调函数需要接收至少一个参数,通常是处理结果或错误信息。其他可选参数根据需要添加。

1.6 回调函数使用

1.6.1 使用Function对象

Function 对象是 JavaScript 中的一种基本数据类型,它可以表示一个函数。可以使用 Function 对象来实现回调函数。如下所示:

js 复制代码
function callbackFunction(param1, param2) {
  // 执行回调函数的逻辑
}

// 将回调函数传递给另一个函数
function asyncFunction(callback) {
  // 执行异步操作
  callback(result1, result2);
}

// 调用异步函数,并将回调函数传递给它
asyncFunction(callbackFunction);

1.6.2 使用arrow函数

arrow函数是 JavaScript 中的一种简化版本的函数表达式,它可以更简洁地定义函数。可以使用 arrow 函数来实现回调函数。如下所示:

js 复制代码
const callbackFunction = (param1, param2) => {
  // 执行回调函数的逻辑
}

// 将回调函数传递给另一个函数
function asyncFunction(callback) {
  // 执行异步操作
  callback(result1, result2);
}

// 调用异步函数,并将回调函数传递给它
asyncFunction(callbackFunction);

1.6.3 使用箭头函数的简写形式

在JavaScript中,箭头函数的简写形式可以用于实现回调函数。实现如下所示:

js 复制代码
const callbackFunction = (param1, param2) => {
  // 执行回调函数的逻辑
}

// 将回调函数传递给另一个函数
function asyncFunction(callback) {
  // 执行异步操作
  callback(result1, result2);
}

// 调用异步函数,并将回调函数传递给它
asyncFunction(callbackFunction);

1.7 回调函数中的this指向

  • 在定时器setIntervalsetTimeout
js 复制代码
var name = 'my name is window';
var obj = {
 name: 'my name is obj',
 fn: function () {
     var timer = null;
     clearInterval(timer);
     timer = setInterval(function () {
         console.log(this.name);  //my name is window
     }, 1000)
 }
}
obj.fn()

在上面这个例子中,我们可以看出来,this的指向是window

如果没有特殊指向,setIntervalsetTimeout的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的。但是平时很多场景下,都需要修改this的指向。这里总结了几种:

  • 最常用的方法:在外部函数中将this存为一个变量,回调函数中使用该变量,而不是直接使用this

    js 复制代码
     var name = 'my name is window';
     var obj = {
         name: 'my name is obj',
         fn: function () {
             var that = this;
             var timer = null;
             clearInterval(timer);
             timer = setInterval(function () {
                 console.log(that.name);  //my name is obj
             }, 1000)
         }
     }
    obj.fn()
  • 使用bind()方法(bind()为ES5的标准,低版本IE下有兼容问题,可以引入es5-shim.js解决)

    bind()的作用类似call和apply,都是修改this指向。但是call和apply是修改this指向后函数会立即执行,而bind则是返回一个新的函数,它会创建一个与原来函数主体相同的新函数,新函数中的this指向传入的对象。

    js 复制代码
     var name = 'my name is window';
     var obj = {
         name: 'my name is obj',
         fn: function () {
             var that = this;
             var timer = null;
             clearInterval(timer);
             timer = setInterval(function () {
                 console.log(that.name);  //my name is obj
             }.bind(this), 1000)
         }
     }
    obj.fn()

    在这里为什么不能用call和apply,是因为call和apply不是返回函数,而是立即执行函数,那么,就失去了定时器的作用。

  • 使用es6的箭头函数:箭头函数的最大作用就是this指向。

    js 复制代码
     var name = 'my name is window';
     var obj = {
         name: 'my name is obj',
         fn: function () {
             var that = this;
             var timer = null;
             clearInterval(timer);
             timer = setInterval(() => {
                 console.log(this.name);  //my name is obj
             }, 1000)
         }
     }
    obj.fn()

    箭头函数没有自己的this,它的this继承自外部函数的作用域。所以,在该例中,定时器回调函数中的this,是继承了fn的this。当然箭头函数也有兼容问题,要是兼容低版本ie,需要使用babel编译,并且引入es5-shim.js才可以。

二、回调地狱

2.1 什么是回调地狱

在回调函数中,执行另一个函数,又在另一个函数中继续执行其它函数,如此层层嵌套,就会出现一个嵌套结构,就会形成回调地狱

js 复制代码
//有多个异步任务,要求需要同时拿到所有异步任务的结果,下边就是用回调地狱
$.get("url", (res1) => {
    conosle.log(res1)
    $.get("url+res1", (res2) => {
        conosle.log(res2)
        $.get("url+res2", (res3) => {
            conosle.log(res3)
            $.get("url+res3", (res4) => {
                conosle.log(res4)
            })
        })
    })
})

上面代码就是一个简单的回调地狱示例,这样的代码肉眼可见的维护性差,可读性也不好

2.2 回调地狱的解决方案

这种层层嵌套的处理逻辑,在有些场景中确实是存在的,虽然我们不能消灭它们,但是我们可以优化它们

2.2.1 Promise 解决回调地狱

Promise是一个对象里面保存着某个未来才会结束的事件就是一个异步操作的结果,从它可以获取异步操作的消息。Promise对象的出现就是进行处理各种异步操作,将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数也就可以用于解决回调地狱。

js 复制代码
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("a");
  }, 1000);
})
  .then((res) => {
    console.log("1秒后打印, res1", res); //1秒后打印 a
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(res + "a");
      }, 1000);
    });
  })
  .then((res) => {
    console.log("2秒后打印, res", res); //2秒后打印 aa
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(res + "a");
      }, 1000);
    });
  })
  .then((res) => {
    console.log("3秒后打印, res3", res); //3秒后打印 aaa
  });

第一个then方法传入的回调函数,返回的是另一个Promise对象。这时,第二个then方法传入的回调函数,就会等待这个新的Promise对象状态发生变化,只有新的Promise对象状态改变了才会触发后面代码的执行。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。这样就将异步操作以同步操作的流程表达出来了

promise解决回调地狱原理 : 在then方法中返回一个promise对象(链式语法嵌套,需要在上一个promise对象的then方法中返回下一个promise)

Promise相关知识可以看我这篇文章:【JavaScript】【ES6】Promise

2.2.2 ES6异步函数async与await

async函数ES2017中引入的更为高级的异步处理机制,可以让异步的处理变的更加便捷,相当于是promise语法的 "高级写法"。

async和await异步函数 : 这两个关键字只能用于函数, 所以用的时候一定要放在函数里面用

  • async关键字: 修饰函数, 表示这个函数内部有异步操作。
  • await关键字: 等待异步执行完毕。

注意点:

  • await 后面是promise对象, 左侧的返回值就是这个promise对象的then方法中的结果
  • await必须要写在async修饰的函数中,不能单独使用,否则程序会报错
js 复制代码
function getPromise(params) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(params)
        }, 1000)
    })
}

const getData = async function() {
    let data1 = await getPromise("a");
    console.log(new Date().getTime(), data1)
    
    let data2 = await getPromise(data1 + "a");
    console.log(new Date().getTime(), data2)
    
    let data3 = await getPromise(data2 + "a");
    console.log(new Date().getTime(), data3)
}

getData()

2.2.3 Fetch

Fetch是XMLHttpRequest的升级版也是单独的一个前端网络请求技术。

Fetch原理是使用Promise语法不使用回调函数,用于在JavaScript脚本里面发出HTTP请求,是浏览器自带的API,后端node.js不能使用。

在用法上,fetch()接受一个 URL 字符串作为参数,默认向该网址发出 GET 请求,返回一个 Promise 对象。fetch()接收到的数据需要先进行数据转码,来得到想要的数据。

多次网络请求需要嵌套时避免层层回调使用方法

js 复制代码
fetch("/ajax3")
.then((response)=>{
    return response.json()//得到 JSON 对象
})
.then((data)=>{
    console.log(data)//真正请求到的数据				
})

2.2.4 Axios网络请求工具

Axios是一个基于Promise对象的网络请求框架,它只是一个网络请求工具并不是网络请求技术,前端网络请求技术有AJAX技术和JSONP技术以及Fetch技术,这个网络请求框架可以用于浏览器和node.js。使用时需要先引入框架到项目中才能使用。这个框架网络请求依然不能解决跨域问题,需要代理服务器才能跨域请求。

多次网络请求需要嵌套时避免层层回调使用方法

js 复制代码
let p1=axios('/ajax1')
    p1.then((data)=>{
        console.log(111111111,data)
        return axios('/ajax2')
    })
    .then((data2)=>{
        console.log(data2.data)
        return axios('/ajax3')
    })
    .then((data3)=>{
        console.log(data3.data)
        return axios('/ajax4')
    })
    .then((data4)=>{
	console.log(data4.data)
    })
    .catch((e)=>{
	console.log(e)
    })

资料来源:

相关推荐
丁总学Java14 分钟前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
It'sMyGo23 分钟前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀25 分钟前
CSS——属性值计算
前端·css
睡觉然后上课34 分钟前
c基础面试题
c语言·开发语言·c++·面试
李是啥也不会39 分钟前
数组的概念
javascript
无咎.lsy1 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec1 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec1 小时前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
豆豆2 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
JUNAI_Strive_ving2 小时前
番茄小说逆向爬取
javascript·python