你真的理解JavaScript中的回调函数吗

392 阅读1分钟

我在前面文章--说说JavaScript中First-Class Function中提到函数在JavaScript中具有很高的地位,被称之为“头等函数First Class”,而回调函数就是其典型体现之一。

在实际开发中,我们经常会去封装各种复用逻辑以及各种算法。而在这些封装过程中,我们要格外注重一个原则就是可扩展性

这里举个栗子(jQuery中each方法):

实际开发时,我们经常会去遍历数组中数据,然后将其展示在页面中,那么我们经常会编写这样的代码:

var data = []
var i = 0;
var l = data.length;

for(;i < l; i++ {
    // ...逻辑代码
}

可以看出,上面代码非常熟悉,因为在项目中的每一个页面我们都有可能会频繁书写很多次。那么聪明的你就会考虑要不要封装一个遍历函数呢。答案是肯定的。

思考一下:在封装时我们要考虑哪些事情呢?

  • 首先封装遍历函数的名字,要见名知意。参考jQuery就叫做each吧
  • 其次函数参数。
    • 待处理的数组对象arr;通过该参数确定我们封装的函数是遍历哪个数组对象的
    • 代码逻辑处理的函数fn;由于每一次数组遍历时对于数组元素处理的逻辑不尽相同,因此这块是体现我们封装的遍历函数是否具有可扩展性。因此用一个函数作为参数,包装调用者的处理逻辑代码。
  • 返回值;因为是功能性封装 是 无需返回值的
  • 最后,在每一次遍历时 都 调用一次 fn,将当前遍历到的数组元素以及索引相关数据传入fn中。在根据fn 的返回值是否为false,来决定循环是否结束。
// 最后简写以下each,fn不能为箭头函数
function each(arr, fn) {
    var i = 0;
    var l = arr.length;

    for (; i < l; i++) {
      if (fn.call(arr, arr[i], i) === false) {
        break;
      }
    }
}

// 使用each
each([1, 2, 3], function (v, i) {
    console.log(i + ':' + v);
});
// output: 0:1 1:2 2:3

什么是回调函数?

在上面封装的each函数中,第二个参数fn就是所谓的回调函数。单从概念上很容易清楚什么是回调函数。即当一个函数fn作为另一个函数func调用时传入的实参时,fn 就是 func的 回调函数

  • 当我们在封装each函数时,默认fn是each使用者传入的。因此在此时我们只需要在合适的时机下调用即可,同时传入该回调函数所需的参数。
  • 当作为each的使用者去调用each时,要去实现该回调函数并传入each中,此时默认已经拿到该回调函数参数值,即参数v为当前元素,i为对应索引值。

因此,我们发现:回调函数fn 是 each的定义者和使用者之间沟通的桥梁。如果两者之间还有消息沟通的话,就通过该回调函数fn的参数来实现。 由于each的使用者在调用时要拿到内部遍历到的每一个元素并进行处理,因此就有消息沟通,也就有了回调函数的参数v和i,分别表示当前元素及其索引值,用来提供给each使用者。

总而言之,想到封装each很简单,但是其回调函数如何更具有扩展性的实现才是重点以及难点。

回调函数的作用

在JavaScript之后各种前端框架包括不限于Vue、React等,都有很多回调函数的涉猎。比如声明周期钩子、路由导航守卫、甚至事件处理等等。

总结一下,当你有以下需求时极有可能需要应用到回调函数

  • 避免重复代码 (DRY—Do Not Repeat Yourself)
  • 在你需要更多的通用功能的地方更好地实现抽象(比如,可处理各种类型的函数)。
  • 增强代码的可读性性以及可维护性
  • 定制的功能

彩蛋

其实,除了上面总结的一些回调函数的使用之外,在Web APIs中也有固定的回调函数使用。比如异步编程。

  • 定时器setTimeout/setInterval中
  • Ajax中

像这样,

// 1s后执行一段代码
setTimeout(() => {
    // 相关代码
}, 1000)
// 每隔1s执行一段代码
setInterval(() => {
    // 相关代码
}, 1s)

// 在ajax中的回调
$.ajax({
    url: '',
    success(data){}, // 成功回调
    error(err){}     // 失败回调
})

这里注意,在Ajax中的回调可能会产生回调地狱问题。这里就不在详细介绍了。

如果这篇文章对你理解回调函数有所帮助的话,可以自己尝试去封装map、filter等这些数组方法,对你理解其底层实现是很有帮助的。