理解与使用Javascript中的回调函数

在Javascript中,函数是第一类对象,这意味着函数能够像对象同样按照第一类管理被使用。既然函数其实是对象:它们能被“存储”在变量中,能做为函数参数被传递,能在函数中被建立,能从函数中返回。javascript

由于函数是第一类对象,咱们能够在Javascript使用回调函数。在下面的文章中,咱们将学到关于回调函数的方方面面。回调函数多是在 Javascript中使用最多的函数式编程技巧,虽然在字面上看起来它们是一小段Javascript或者jQuery代码,可是对于许多开发者来讲它任然是一个谜。在阅读本文以后你能了解怎样使用回调函数。java

回调函数是从一个叫函数式编程的编程范式中衍生出来的概念。简单来讲,函数式编程就是使用函数做为变量。函数式编程过去 - 甚至是如今,依旧没有被普遍使用 - 它过去常被看作是那些受过特许训练的,大师级别的程序员的秘传技巧。node

幸运的是,函数是编程的技巧如今已经被充分阐明,所以像我和你这样的普通人也能去轻松使用它。函数式编程中的一个主要技巧就是回调函数。在后面内容中 你会发现实现回调函数其实就和普通函数传参同样简单。这个技巧是如此的简单以至于我经常感到很奇怪为何它常常被包含在讲述Javascript高级技巧的章节中。程序员

什么是回调或者高阶函数

一个回调函数,也被称为高阶函数,是一个被做为参数传递给另外一个函数(在这里咱们把另外一个函数叫作“otherFunction”)的函数,回调函数在otherFunction中被调用。一个回调函数本质上是一种编程模式(为一个常见问题建立的解决方案),所以,使用回调函数也叫作回调模式。web

下面是一个在jQuery中使用回调函数简单广泛的例子:ajax

/注意到click方法中是一个函数而不是一个变量
//它就是回调函数
$("#btn_1").click(function() {
  alert("Btn 1 Clicked");
});

正如你在前面的例子中看到的,咱们将一个函数做为参数传递给了click方法。click方法会调用(或者执行)咱们传递给它的函数。这是Javascript中回调函数的典型用法,它在jQuery中普遍被使用。mongodb

下面是另外一个Javascript中典型的回调函数的例子:shell

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

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

再一次,注意到咱们讲一个匿名函数(没有名字的函数)做为参数传递给了forEach方法。编程

到目前为止,咱们将匿名函数做为参数传递给了另外一个函数或方法。在咱们看更多的实际例子和编写咱们本身的回调函数以前,先来理解回调函数是怎样运做的。数组

回调函数是怎样运做的?

由于函数在Javascript中是第一类对象,咱们像对待对象同样对待函数,所以咱们能像传递变量同样传递函数,在函数中返回函数,在其余函数中使用函数。当咱们将一个回调函数做为参数传递给另外一个函数时,咱们仅仅传递了函数定义(只传递了函数名称),并无在参数中执行函数。回调函数并不会立刻被执行,它会在包含它的函数内的某个特定时间点被“回调”(就像它的名字同样)。所以,即便第一个jQuery的例子以下所示:

//匿名函数不会在参数中被执行
//这是一个回调函数    
$("#btn_1").click(function(){
    alert("Btn 1 Clicked");
});

这个匿名函数稍后会在函数体内被调用。即便有名字,它依然在包含它的函数内经过arguments对象获取。

回调函数是闭包

当咱们将一个回调函数做为变量传递给另外一个函数时,这个回调函数在包含它的函数内的某一点执行,就好像这个回调函数是在包含它的函数中定义的同样。这意味着回调函数本质上是一个闭包。正如咱们所知,闭包可以进入包含它的函数做用域,所以回调函数能获取包含它函数中的变量,以及全局做用域中的变量。

实现回调函数的基本原理

回调函数并不复杂,可是在咱们开始建立并使用回调函数以前,咱们应该熟悉几个实现回调函数的基本原理。

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

在前面的jQuery例子以及forEach的例子中,咱们使用了在参数位置定义匿名函数做为回调函数。这是在回调函数使用中的一种广泛的魔术。另外一种常见的模式是定义一个命名函数并将函数名做为变量传递给函数。好比下面的例子:

//全局变量
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, callback){
    allUserData.push(options);
    callback(options);
}

//当咱们调用getInput函数时,咱们将logStuff做为一个参数传递给它     
//所以logStuff将会在getInput函数内被回调(或者执行)     
getInput({name:"Rich",speciality:"Javascript"}, logStuff);
//name:Rich
//speciality:Javascript

传递参数给回调函数

既然回调函数在执行时仅仅是一个普通函数,咱们就能给它传递参数。咱们可以传递任何包含它的函数的属性(或者全局书讯给)做为回调函数的参数。在前面的例子中,咱们将options做为一个参数传递给了回调函数。如今咱们传递一个全局变量和一个本地变量:

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

function getInput (options, callback){
    allUserData.push (options);
    //将全局变量generalLastName传递给回调函数
    callback(generalLastName,options);
}

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

在调用以前检查做为参数被传递的回调函数确实是一个函数,这样的作法是明智的。同时,这也是一个实现条件回调函数的最佳时间。

咱们来重构上面例子中的getInput函数来确保检查是恰当的。

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

    //确保callback是一个函数    
    if(typeof callback === "function"){
        //调用它,既然咱们已经肯定了它是可调用的
          callback(options);
    }
}

若是没有适当的检查,当getInput的参数中没有一个回调函数或者传递的回调函数事实上并非一个函数,代码将会致使运行错误。

使用this对象的方法做为回调函数时的问题

当回调函数是一个this对象的方法时,咱们必须改变执行回调函数的方法来保证this对象的上下文。不然若是回调函数被传递给一个全局函数,this对象要么指向全局window对象(在浏览器中)。要么指向包含方法的对象。 咱们在下面的代码中说明:

  //定义一个拥有一些属性和一个方法的对象,咱们接着将会把方法做为回调函数传递给另外一个函数

var clientData = {
    id: 094545,
    fullName: "Not Set",
    //setUsrName是一个在clientData对象中的方法
    setUserName: function(firstName, lastName){
        //这指向了对象中的fullName属性
        this.fullName = firstName + " " + lastName;
    }
} 

function getUserInput(firstName, lastName, callback){
    //在这作些什么来确认 firstName/lastName
    //如今存储names
    callback(firstName, lastName);
}

在下面你的代码例子中,当clientData.setUsername被执行时,this.fullName并无设置clientData对象 中的fullName属性。相反,它将设置window对象中的fullName属性,由于getUserInput是一个全局函数。这是由于全局函数中 的this对象指向window对象。

getUserInput("Barack","Obama",clientData.setUserName);
console.log(clientData.fullName);  //Not Set
//fullName属性将在window对象中被初始化     
console.log(window.fullName);  //Barack Obama

使用Call和Apply函数来保存this

咱们可使用Call或者Apply函数来修复上面你的问题。咱们知道每一个Javascript中的函数都有两个方法:Call 和 Apply,这些方法被用来设置函数内部的this对象以及给此函数传递变量。call接收的第一个参数为被用来在函数内部当作this的对象,传递给函数的参数被挨个传递(使用逗号分开)。Apply函数的第一个参数也是在函数内部做为this的对象,然而最后一个参数倒是传递给函数值的数组。听起来很复杂,那么咱们来看看使用Apply和Call有多么的简单。为了修复前面例子的问题,我将在下面你的例子中使用Apply函数:

//注意到咱们增长了新的参数做为回调对象,叫作“callbackObj”
function getUserInput(firstName, lastName, callback,callbackObj){
        //在这里作些什么来确认名字
        callback.apply(callbackObj, [firstName, lastName]);
}

使用Apply函数正确设置了this对象,咱们如今正确的执行了callback并在clientData对象中正确设置了fullName属性:

//咱们将clientData.setUserName方法和clientData对象做为参数,clientData对象会被Apply方法使用来设置this对象     
getUserName("Barack", "Obama", clientData.setUserName, clientData);

//clientData中的fullName属性被正确的设置
console.log(clientUser.fullName); //Barack Obama

咱们也可使用Call函数,可是在这个例子中咱们使用Apply函数。

容许多重回调函数

咱们能够将不止一个回调函数做为参数传递给另外一个函数,就像咱们可以传递多个变量同样。这里有一个关于jQuery中AJAX的例子:

function successCallback(){
    //在发送以前作点什么
}     

function successCallback(){
  //在信息被成功接收以后作点什么
}

function completeCallback(){
  //在完成以后作点什么
}

function errorCallback(){
    //当错误发生时作点什么
}

$.ajax({
    url:"http://fiddle.jshell.net/favicon.png",
    success:successCallback,
    complete:completeCallback,
    error:errorCallback

});

“回调地狱”问题以及解决方案

在执行异步代码时,不管以什么顺序简单的执行代码,常常状况会变成许多层级的回调函数堆积以至代码变成下面的情形。这些杂乱无章的代码叫作回调地狱. 由于回调太多而使看懂代码变得很是困难。我从node-mongodb-native,一个适用于Node.js的MongoDB驱动中拿来了一个例子。 这段位于下方的代码将会充分说明回调地狱:

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. 模块化L将你的代码分隔到模块中,这样你就能够处处一块代码来完成特定的工做。而后你能够在你的巨型应用中导入模块。

建立你本身的回调函数

既然你已经彻底理解了关于Javascript中回调函数的一切(我认为你已经理解了,若是没有那么快速的重读),你看到了使用回调函数是如此的简单而强大,你应该查看你的代码看看有没有能使用回调函数的地方。回调函数将在如下几个方面帮助你:
- 避免重复代码(DRY-不要重复你本身) - 在你拥有更多多功能函数的地方实现更好的抽象(依然能保持全部功能) - 让代码具备更好的可维护性
- 使代码更容易阅读
- 编写更多特定功能的函数

建立你的回调函数很是简单。在下面的例子中,我将建立一个函数完成如下工做:读取用户信息,用数据建立一首通用的诗,而且欢迎用户。这原本是个很是复杂的函数由于它包含不少if/else语句而且,它将在调用那些用户数据须要的功能方面有诸多限制和不兼容性。

相反,我用回调函数实现了添加功能,这样一来获取用户信息的主函数即可以经过简单的将用户全名和性别做为参数传递给回调函数并执行来完成任何任务。

简单来说,getUserInput函数是多功能的:它能执行具备无种功能的回调函数。

//首先,建立通用诗的生成函数;它将做为下面的getUserInput函数的回调函数

function genericPoemMaker(name, gender) {
        console.log(name + " is finer than fine wine.");
        console.log("Altruistic 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");
    }

        //callback,参数的最后一项,将会是咱们在上面定义的genericPoemMaker函数
        function getUserInput(firstName, lastName, gender, callback) {
            var fullName = firstName + " " + lastName;

            // Make sure the callback is a function
            if (typeof callback === "function") {
            // Execute the callback function and pass the parameters to it
            callback(fullName, gender);
            }
        }    



调用getUserInput函数并将genericPoemMaker函数做为回调函数:   

    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函数:

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

// 将greetUser做为一个回调函数
getUserInput("Bill", "Gates", "Man", greetUser);

// 这里是输出
Hello, Mr. Bill Gates

咱们调用了彻底相同的getUserInput函数,可是此次完成了一个彻底不一样的任务。

正如你所见,回调函数很神奇。即便前面的例子相对简单,想象一下能节省多少工做量,你的代码将会变得更加的抽象,这一切只须要你开始使用回调函数。

在Javascript编程中回调函数常常以几种方式被使用,尤为是在现代web应用开发以及库和框架中:

  • 异步调用(例如读取文件,进行HTTP请求,等等)
  • 时间监听器/处理器
  • setTimeout和setInterval方法
  • 通常状况:精简代码

结束语

Javascript回调函数很是美妙且功能强大,它们为你的web应用和代码提供了诸多好处。你应该在有需求时使用它;或者为了代码的抽象性,可维护性以及可读性而使用回调函数来重构你的代码。

相关文章
相关标签/搜索