文章目录

  1. 1. 什么是回调函数
  2. 2. 回调函数是如何实现
  3. 3. 回调函数的基本原则
    1. 3.1. 使用命名函数或匿名函数作为回调
    2. 3.2. 传递参数给回调函数
    3. 3.3. 在执行之前确保回调是一个函数
    4. 3.4. 使用含有this对象的回调函数的问题
    5. 3.5. 多重回调函数也是允许的
  4. 4. “回调地狱”的问题和解决方案
  5. 5. 实现自己的回调函数
  6. 6. 参考文献

我们可以将它作为参数传递给另一个函数,延迟到函数中执行,甚至执行后将它返回。这是在JavaScript中使用回调函数的精髓。本篇文章的将全面学习JavaScript的回调函数。回调函数也许是JavaScript中使用最广泛的功能性编程技术,也许仅仅一小段JavaScript或jQuery的代码都会给开发者留下一种神秘感,阅读这篇文章后,也许会帮你消除这种神秘感。

什么是回调函数

思考一下, 下面这种在jQuery中常用的回调函数。

$("#btn").click(function() {
    alert("Btn 1 Clicked");
});
还有一个例子
var friends = ["Mike", "Stacy", "Andy", "Rick"];
friends.forEach(function (eachName, index){
    console.log(index + 1 + ". " + eachName); 
});

到目前为止,我们传递了一个匿名的函数作为参数给另一个函数或方法。在看其它更复杂的回调函数之前,让我们理解一下回调的工作原理并实现一个自己的回调函数。

回调函数是如何实现

我们可以像使用变量一样使用函数,作为另一个函数的参数,在另一个函数中作为返回结果,在另一个函数中调用它。当我们作为参数传递一个回调函数给另一个函数时,我们只传递了这个函数的定义,并没有在参数中执行它。
当包含(调用)函数拥有了在参数中定义的回调函数后,它可以在任何时候调用(也就是回调)它。这说明回调函数并不是立即执行,而是在包含函数的函数体内指定的位置“回调”它。
回调函数是闭包的,当作为参数传递一个回调函数给另一个函数时,回调函数将在包含函数函数体内的某个位置被执行,就像回调函数在包含函数的函数体内定义一样。闭包函数可以访问包含函数的作用域,所以,回调函数可以访问包含函数的变量,甚至是全局变量。

回调函数的基本原则

使用命名函数或匿名函数作为回调

在前面的jQuery和forEach的例子中,我们在包含函数的参数中定义匿名函数,这是使用回调函数的通用形式之一,另一个经常被使用的形式是定义一个带名称的函数,并将函数名作为参数传递给另一个函数。

var allUserData = [];
function logStuff (userData) {}
function getInput (options, callback) {}
getInput ({name:"Rich", speciality:"JavaScript"}, logStuff);

传递参数给回调函数

因为回调函数在执行的时候就和一般函数一样,我们可以传递参数给它。可以将包含函数的任何属性(或全局的属性)作为参数传递回调函数。下面的例子让我们传递一个全局变量或本地变量给回调函数。

var generalLastName = "Clinton";
function getInput (options, callback) {
    callback (generalLastName, options);
}

在执行之前确保回调是一个函数

在调用之前,确保通过参数传递进来的回调是一个需要的函数通常是明智的。此外,让回调函数是可选的也是一个好的实践。让我们重构一下上面例子中的getInput函数,确保回调函数做了适当的检查。

function getInput(options, callback) {
    if (typeof callback === "function") {
        callback(options);
    }
}

如果getInput函数没有做适当的检查(检查callback是否是函数,或是否通过参数传递进来了),我们的代码将会导致运行时错误。

使用含有this对象的回调函数的问题

当回调函数是一个含有this对象的方法时,我们必须修改执行回调函数的方法以保护this对象的内容。否则this对象将会指向全局的window对象(如果回调函数传递给了全局函数),或指向包含函数。让我们看看下面的代码:

var clientData = {
    id: 094545,
    fullName: "Not Set",
    setUserName: function (firstName, lastName)  {
      this.fullName = firstName + " " + lastName;
    }
}
function getUserInput(firstName, lastName, callback)  {
    callback (firstName, lastName);
}
在下面的示例代码中,当clientData.setUserName被执行时,this.fullName不会设置clientData 对象中的属性fullName,而是设置window 对象中的fullName,因为getUserInput是一个全局函数。出现这种现象是因为在全局函数中this对象指向了window对象。
getUserInput ("Barack", "Obama", clientData.setUserName);
console.log (clientData.fullName);
console.log (window.fullName);
使用Call或Apply函数保护this对象 我们可以通过使用 Call 或 Apply函数来解决前面示例中的问题。到目前为止,我们知道JavaScript中的每一个函数都有两个方法:Call和Apply。这些方法可以被用来在函数内部设置this对象的内容,并内容传递给函数参数指向的对象。这听起来很复杂,但让我们看看Apply和Call的使用是多么容易。为解决前面例子中出现的问题,我们使用Apply函数如下。
function getUserInput(firstName, lastName, callback, callbackObj)  {
    callback.apply (callbackObj, [firstName, lastName]);
}

通过Apply函数正确地设置this对象,现在我们可以正确地执行回调函数并它正确地设置clientData对象中的fullName属性。

多重回调函数也是允许的

我们可以传递多个回调函数给另一个函数,就像传递多个变量一样。这是使用jQuery的AJAX函数的典型例子:

function successCallback() {
    // Do stuff before send
}
function successCallback() {
    // Do stuff if success message received
}
function completeCallback() {
    // Do stuff upon completion
}
function errorCallback() {
    // Do stuff if error received
}
$.ajax({
    url:"http://www.lianggzone.com",
    success:successCallback,
    complete:completeCallback,
    error:errorCallback
});

“回调地狱”的问题和解决方案

异步代码执行是一种简单的以任意顺序执行的方式,有时是很常见的有很多层级的回调函数,你看起来像下面这样的代码。下面这种凌乱的代码称作“回调地狱”,因为它是一种包含非常多的回调的麻烦的代码。我是在node-mongodb-native里看到这个例子的,MongoDB驱动Node.js.示例代码就像这样:

var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory});
p_client.open(function(err, p_client) {
    p_client.dropDatabase(function(err, done) {
        p_client.createCollection('test_custom_key', function(err, collection) {
            collection.insert({'a':1}, function(err, docs) {
                collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) {
                    cursor.toArray(function(err, items) {
                        test.assertEquals(1, items.length);
                        // Let's close the db
                        p_client.close();
                    });
                });
            });
        });
    });
});

你不太可能在自己的代码里碰到这个的问题,但如果你碰到了(或以后偶然碰到了),那么有以下两种方式解决这个问题。

  • 命名并定义你的函数,然后传递函数名作为回调,而不是在主函数的参数列表里定义一个匿名函数。
  • 模块化:把你的代码划分成一个个模块,这样你可以空出一部分代码块做特殊的工作。然后你可以将这个模型引入到你的大型应用程序中。

实现自己的回调函数

现在你已经完全理解了JavaScript关于回调的所用特性并且看到回调的使用是如此简单但功能却很强大。你应该看看自己的代码是否有机会使用回调函数,有以下需求时你可以考虑使用回调:

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

我们再看下下面这个例子。

function getUserInput(firstName, lastName, callback) {
    var fullName = firstName + " " + lastName;
    if (typeof callback === "function") {
        callback(fullName);
    }
}

如你所见,回调函数提供了广泛的功能。尽管前面提到的例子非常简单,在你开始使用回调函数的时候思考一下你可以节省多少工作,如何更好地抽象你的代码。加油吧!
我们在JavaScript中经常使用回调函数时注意以下几点,尤其是现在的web应用开发,在第三方库和框架中。

  • 异步执行(例如读文件,发送HTTP请求)
  • 事件监听和处理
  • 设置超时和时间间隔的方法
  • 通用化:代码简洁

参考文献

(书)「编写可维护的JavaScript」(Nicholas C. Zakas)

(完)

微信公众号

文章目录

  1. 1. 什么是回调函数
  2. 2. 回调函数是如何实现
  3. 3. 回调函数的基本原则
    1. 3.1. 使用命名函数或匿名函数作为回调
    2. 3.2. 传递参数给回调函数
    3. 3.3. 在执行之前确保回调是一个函数
    4. 3.4. 使用含有this对象的回调函数的问题
    5. 3.5. 多重回调函数也是允许的
  4. 4. “回调地狱”的问题和解决方案
  5. 5. 实现自己的回调函数
  6. 6. 参考文献