[转载]理解 JavaScript 回调函数并使用

JavaScript中,函数是一等(first-class)对象;也就是说,函数是 Object 类型并且可以像其他一等对象(String,Array,Number等)一样使用。它们可以“保存在变量中,作为参数传递给函数,在函数内创建,以及被函数返回”。

由于函数是一等对象,我们可以把一个函数作为参数传递给另一个函数,然后在那个函数内执行,甚至也可以被那个函数返回,然后再执行。这就是 JavaScript 中回调函数(callback functions)的本质。在本文的剩余部分,我们将学习到关于 JavaScript 回调函数的所有知识。回调函数可能是 JavaScript 中使用最广泛的函数式编程技术了,你可以在任何一段 JavaScript或jQuery 代码发现它,但是,它对很多 JavaScript 开发者来说依然是神秘的。直到你阅读了本文,就再也不会对它感到神秘了。

回调函数 是来源于函数式编程的一种技术。从底层来说,函数式编程把函数用作参数。函数式编程过去是 —— 现在仍然是,(尽管如今不太流行)被有经验的、高级开发者视作难懂的技术。

幸运的是,函数式编程已经被阐明到像我们这样的的普通人都能容易理解的地步。其主要技术之一就是回调函数。下面你就会看到,实现回调函数很容易,就像传递一个普通变量参数一样。这个技术如此简单以至于我很奇怪为什么大多数教程都把它归类为高级主题里面。

回调是什么?

回调函数,也叫高阶(higher-order)函数,是一个作为参数传递到其他函数的函数,然后回调函数在其他函数中被调用。回调函数本质上是一个模式,因此使用回调函数被称为回调模式。

请看下面的代码,这是 jQuery 中常见的回调函数的使用:

$("#btn_1").click(function(){
    alert("Btn 1 Clicked");
});

这里,传递了一个匿名函数给 click 方法。click 方法将调用或执行这个回调函数。

再看一个例子:

var friend = ["Mike", "Stacy", "Andy", "Rick"];

friend.forEach(function(eachName, index){
    console.log(index + 1 + "." + eachName); // 1.Mike,2.Stacy,3.Andy,4.Rick
});

这里,传递了一个匿名函数给 forEach 方法。

回调函数如何工作?

当我们传递一个回调函数给其他函数时,我们只是传递了函数定义。我们并没有在参数中执行函数。换句话说,传递函数时不能在函数名后面加括号“()”,而执行函数时那样需要。

由于其他函数在参数中有该回调函数的定义,所以它可以在任何时候执行该函数。

注意到回调函数不是立即执行的,所谓“回调”就是指在其他函数中某个特定的时候被回头调用。所以再看第一个例子,click 函数中的匿名函数将在click函数体内被调用。即使该函数匿名,也可以通过 arguments 对象访问到。

回调函数都是闭包
当传递一个回调函数给其他函数时,回调函数在其他函数函数体内某处执行,就好像回调函数是在其他函数中定义的。这说明回调函数是一个闭包(closure)。众所周知,闭包可以访问外层包含函数的作用域,所以回调函数也能访问其他函数的变量,甚至全局作用域中的变量。

实现回调函数所遵循的基本准则

尽管不复杂,但仍然有一些值得注意的地方。

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

第二个例子中我们使用了匿名函数作为回调函数,这是一种常见的做法。另一种常见做法是定义一个有名字的函数,然后传递给另一个函数。

// 全局变量
var allUserData = [];

// logStuff 函数,用于打印参数的值
function logStuff(userData){
    if(typeof userData === "string"){
        console.log(userData);
    }
    else if(typeof userData === "object"){
        for(var item in userData){
            console.log(item + ":" + userData[item]);
        }
    }
}

// 该函数接受两个参数,第二个参数是回调函数
function getInput(options, calllback){
    allUserData.push(options);
    calllback(options);
}

// 调用 getInput 函数时传递了 logStuff,所以,logStuff 将在 getInput 函数中被调用(或执行)
getInput({name:"Rich",speciality:"JavaScript"},logStuff);
// name:Rich
// speciality:JavaScript

传递参数给回调函数

由于回调函数在执行时也只是一个普通函数,所以我们可以传递参数给它。可以传递外层函数的属性或者全局变量。上例中,传递了 options 作为回调函数的参数。然后,让我们传递一个全局变量和局部变量。

// 全局变量
var generalLastName = "Clinton";

function getInput(options, calllback){
    allUserData.push(options);
    // 传递全局变量
    calllback(generalLastName, options);
}

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

在调用之前检查所传入的回调函数是否真的是一个函数是一个好习惯。让我们重构一下上例中getInput函数:

function getInput(options, calllback){
    allUserData.push(options);

    // 确保 calllback 是一个函数
    if(typeof calllback === "function"){
        // 调用之,因为已经确保它是函数了
        calllback(options);
    }
}

如果不检测其类型,当传入的参数不是函数时,就会导致运行时错误。

回调函数与this相关的问题

当回调函数中用到了 this 对象时,我们不得不改变执行回调函数的方式来保持原有的 this 对象。否则,this 对象可能指向全局的 window 对象,如果回调函数被传入了全局函数中的话。或者指向外层包含方法的对象。

下面在代码中演示:

var clientData = {
    id: 012334,
    fullName: "Not Set",
    // setUserName 是 clientData 对象的方法
    setUserName: function(firstName, lastName){
        this.fullName = firstName + " " + lastName;
    }
}

function getUserInput(firstName, lastName, calllback){
    calllback(firstName,lastName);
}

下面的代码中,当 clientData.setUserName 执行时,this.fullName 将不会设置 clientData 对象的 fullName 属性,而是设置为 window 对象的 fullName 属性。这是因为全局函数中的 this 对象指向 window 对象

getUserInput("Barack", "Obama", clientData.setUserName);

console.log(clientData.fullName); // Not Set

console.log(window.fullName); // Barack Obama

使用 CallApply 函数来保持 this

我们可以通过 CallApply 函数来解决上面的问题。目前来说,JavaScript 中的每个函数都有两个方法:Call 和 Apply。这两个方法用来设置函数内的 this 对象。

Call 把第一个参数作为函数内的 this 对象,其他的参数分别传递给函数。Apply 也是把第一个参数作为函数内的 this 对象,而第二个参数是一个数组(或者argument对象)。

我们在下面的代码中使用 Apply 来解决这个问题:

function getUserInput(firstName, lastName, calllback, calllbackObj){
    calllback.apply(calllbackObj, [firstName,lastName]);
}

apply 正确设置了 this 对象,现在可以正确执行回调,并设置 clientData 上的 fullName 属性了。

getUserInput("Barack", "Obama", clientData.setUserName, clientData);

console.log(clientData.fullName); // Barack Obama

允许使用多个回调函数

我们传递多个回调函数到另外函数,就像传递多个变量一样,下面时一个经典的jQuery AJAX函数的例子:

function successCallBack(){

}

function failCallBack(){

}

function completeCallback(){

}

function errorCallback(){

}

$.ajax({
    url:"http://www.91ymb.com/favicon.png",
    success: successCallBack,
    complete: completeCallback,
    error: errorCallback
});

“回调地狱”问题和解决

在异步代码执行中,代码可能以任何顺序执行,有时会看到有很多层回调函数,比如下例:

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();
                    });
                });
            });
        });
    });
});

以上杂乱的代码被称作回调地狱,回调太多以至于很难理解。你可能不会遭遇这个问题,但是如果遇到了,这有两种方法解决这个问题。

  1. 命名函数,定义函数,然后传递函数名作为回调,而不是定义匿名函数。
  2. 模块化:把代码分模块,这有就可以导出某个特定任务的代码。然后导入那个特定模块。

写你自己的回调函数

到现在,你应该理解了关于 JavaScript 回调函数的所有内容,你发现使用回调函数不仅简单而且很强大,你应该看看自己的代码,寻找一些机会使用回调函数,它能让你做这些事情:

  1. 不重复代码(DRY)
  2. 实现更好的抽象。
  3. 更好的可维护性
  4. 更好的可读性
  5. 更多专门的函数

写自己的回调函数也很简单。下面的例子中,我将创建一个函数用来:取回用户数据,使用数据生成诗句,然后告诉用户。这听起来好像是一个杂乱的函数,有很多if/else语句,并且可能被限制而不能用用户数据做些其他的事情。

但是,我把具体功能的实现交给回调函数,这样主函数用于取回用户数据,然后简单地传递用户全名和性别给回调函数,然后执行回调函数就行了。

简言之,getUserInput 函数是通用的:它执行所有回调函数来实现具体的功能。

// 首先,建立一个诗句生成函数,它将作为回调函数传递
function genericPoemMaker(name, gender){
    console.log(name + " is finer than fine wine.");
    console.log("ALltrustic and noble for the modern time.");
    console.log("Always admirably adorned with the latest style.");
    console.log("A " + gender + " of unfortunate tragedies who still manages a perpetual smile");
}

// 
function getUserInput(firstName, lastName, gender, callback){
    var fullName = firstName + " " + lastName;

    // 确保 callback 是函数
    if(typeof callback === "function"){
        callback(fullName, callback);
    }
}

// 调用 getUserInput,并传递回调函数
getUserInput("Michael", "Fassbender", "Man", genericPoemMaker);
// 输出
​/* Michael Fassbender is finer than fine wine.
Altruistic and noble for the modern time.
Always admirably adorned with the latest style.
A Man of unfortunate tragedies who still manages a perpetual smile.
*/

由于 getUserInput 函数只是处理数据,我们可以传递任何回调函数。例如,传递一个 greetUser 函数:

function greetUser(customerName, sex){
    var salutation = sex & sex == "Man" ? "Mr." : "Ms.";
    console.log("Hello, " + salutation + " " + customerName);
}

getUserInput("Bill", "Gates", "Man", greetUser);

// 输出
// Hello, Mr. Bill Gates

我们同样调用 getUserInput 函数两次,但执行了不同的任务。

注意到,下列场景是我们频繁使用到回调函数的地方,尤其是现代 web 应用开发,库和框架开发:

  1. 异步执行(如读取文件,HTTP请求)
  2. 事件监听器/处理器
  3. setTimeout 函数和 setInterval 函数
  4. 通用原则:代码简洁性

最后

JavaScript 回调函数很好用,有很多好处。现在就开始使用回调函数来重构代码以提高抽象、可维护性、可读性吧。

英文原文: http://javascriptissexy.com/understand-javascript-callback-functions-and-use-them/