10分钟了解回调函数
2024-12-09 11:13:16

面试官:既然您提到了回调函数,能简单说一下回调函数吗?

我:就是将函数作为参数传递,让函数可以被其他函数使用。

面试官:能再详细说说吗?您作为一个老前端,相比理解的很透彻吧?

我:哈哈哈哈,不能。

面试官:哈哈哈哈哈,您真幽默,和您聊的很开心,回去等通知吧。

正文

我真没想到,我会再次因为这种古董面试题被摆了一道,五年前我能说的头头是道的面试题,如今居然被卡了。

哎,面试失败之后,立刻回家重新整理了一下相关的文档。

什么是回调函数

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

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

这里我们写个回调函数被调用的例子

1
2
3
4
5
6
7
8
9
10
11
12
function myDisplayer(some) {
document.getElementById("demo").innerHTML = some;
}

// myCallback 就是回调函数的传参,我们还可以给回调函数传参
function myCalculator(num1, num2, myCallback) {
let sum = num1 + num2;
console.log('得到结果:'+sum);
myCallback(sum);
}

myCalculator(5, 5, myDisplayer);

【注意点】

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

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

回调函数是一种常见的编程技术,他可以在异步操作完成后调用一个预定义的函数来处理结果。

回调函数通常用于处理事件、执行异步操作或响应用户输入等场景。

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

回调函数的优缺点

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

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

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

回调函数和闭包的关系

回调函数和闭包之间存在着紧密的关系。

回调函数是一个函数,在另一个函数中被作为参数传递,并在该函数执行完成后被调用。

闭包是由一个函数及其相关的引用环境组合而成的实体,可以访问函数外部的变量。

在某些情况下,回调函数需要访问到它所在的父函数的变量,这时就需要使用闭包来实现。

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

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

回调函数和Promise的关系

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

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

而Promise则是一种更加高级的异步编程模式,它通过解决回调地狱问题,提供了更加优雅和简洁的异步编程方式。

Promise可以将异步操作封装成一个Promise对象,并通过链式调用then()方法来注册回调函数,以及catch()方法来捕获异常。当异步操作完成后,Promise会自动根据操作结果触发相应的回调函数。

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

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

回调函数和观察者模式都是用于实现事件驱动编程的技术。

它们之间的关系是,观察者模式是一种设计模式,它通过定义一种一对多的依赖关系,使得一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。

而回调函数则是一种编程技术,它允许将一个函数作为参数传递给另一个函数,在执行过程中调用这个函数来完成特定的任务。

在观察者模式中,当一个被观察的对象发生改变时,会遍历所有的观察者对象,调用其定义好的更新方法,以进行相应的操作。

这里的更新方法就可以看做是回调函数,因为它是由被观察对象调用的,并且在执行过程中可能需要使用到一些外部参数或上下文信息。因此,可以说观察者模式本身就包含了回调函数的概念,并且借助回调函数来实现观察者模式的具体功能。

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

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

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

回调函数的命名规范

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

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

回调函数的参数设计

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

回调函数使用

使用Function对象

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

1
2
3
4
5
6
7
8
9
10
11
12
function callbackFunction(param1, param2) {
// 执行回调函数的逻辑
}

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

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

使用arrow(箭头)函数

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

1
2
3
4
5
6
7
8
9
10
11
12
const callbackFunction = (param1, param2) => {
// 执行回调函数的逻辑
}

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

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

回调函数中的this指向

  • 在定时器setIntervalsetTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()

// undefined
// my name is window

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

如果没有特殊指向,setIntervalsetTimeout的回调函数中this的指向都是window,这是因为JS的定时器方法是定义在window下的。

但是平时很多场景下,都需要修改this的指向。这里总结了几种:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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()

    // undefined
    // my name is window
  • 使用bind()方法(bind()为ES5的标准,低版本IE下有兼容问题,可以引入es5-shim.js解决)

    bind()的作用类似call和apply,都是修改this指向。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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()

    // undefined
    // my name is window

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     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才可以。

结语

回调函数其实本质上不能算是JS的独有手法,只是一种入门的编程思想罢了。

虽然面试官让我详细说说我依然感觉很荒诞,但是我没能详细说说,确实不太符合老年程序员的身份。

如今回顾了一下,倒也是颇为感慨,我的确挖的不够深,大开眼界。

参考

【JavaScript】【回调】回调函数 && 回调地狱 - 掘金 (juejin.cn)

回调函数(callback)是什么?一文理解回调函数(callback)

一文告诉你什么是回调地狱,如何解决回调地狱?-CSDN博客

回调地狱以及解决回调地狱 - 掘金 (juejin.cn)