详解JavaScript的回调函数
本文的目录:
- 什么是回调或高级函数
- 回调函数是如何实现的
- 实现回调函数的基本原则
- 回调地狱的问题和解决方案
- 实现自己的回调函数
在JavaScrip中,function是内置的类对象,也就是说它是一种类型的对象,可以和其它String、Array、Number、Object类的对象一样用于内置对象的管理。因为function实际上是一种对象,它可以“存储在变量中,通过参数传递给(别一个)函数(function),在函数内部创建,从函数中返回结果值”。
因为function是内置对象,我们可以将它作为参数传递给另一个函数,延迟到函数中执行,甚至执行后将它返回。这是在JavaScript中使用回调函数的精髓。本篇文章的剩余部分将全面学习JavaScript的回调函数。回调函数也许是JavaScript中使用最广泛的功能性编程技术,也许仅仅一小段JavaScript或jQuery的代码都会给开发者留下一种神秘感,阅读这篇文章后,也许会帮你消除这种神秘感。
回调函数来自一种著名的编程范式——函数式编程,在基本层面上,函数式编程指定的了函数的参数。函数式编程虽然现在的使用范围变小了,但它一直被“专业的聪明的”程序员看作是一种难懂的技术,以前是这样,未来也将是如此。
幸运的是,函数式编程已经被阐述的像你我这样的一般人也能理解和使用。函数式编程最主要的技术之一就是回调函数,你很快会阅读到,实现回调函数就像传递一般的参数变量一样简单。这项技术如此的简单,以至于我都怀疑为什么它经常被包含在JavaScript的高级话题中去。
一、什么是回调或高级函数?
回调函数被认为是一种高级函数,一种被作为参数传递给另一个函数(在这称作"otherFunction")的高级函数,回调函数会在otherFunction内被调用(或执行)。回调函数的本质是一种模式(一种解决常见问题的模式),因此回调函数也被称为回调模式。
思考一下下面这种在jQuery中常用的回调函数:
//Notethattheitemintheclickmethod'sparameterisafunction,notavariable. //Theitemisacallbackfunction $("#btn_1").click(function(){ alert("Btn1Clicked"); });
正如在前面的例子所看到的,我们传递了一个函数给click方法的形参,click方法将会调用(或执行)我们传递给它的回调函数。这个例子就给出了JavaScript中使用回调函数的一个典型方式,并广泛应用于jQuery中。
细细体味一下另一个基本JavaScript的典型例子:
varfriends=["Mike","Stacy","Andy","Rick"]; friends.forEach(function(eachName,index){ console.log(index+1+"."+eachName);//1.Mike,2.Stacy,3.Andy,4.Rick });
我们再一次用同样的方式传递了一个匿名的函数(没有函数名的函数)给forEach方法,作为forEach的参数。
到目前为止,我们传递了一个匿名的函数作为参数给另一个函数或方法。在看其它更复杂的回调函数之前,让我们理解一下回调的工作原理并实现一个自己的回调函数。
二、回调函数是如何实现的?
我们可以像使用变量一样使用函数,作为另一个函数的参数,在另一个函数中作为返回结果,在另一个函数中调用它。当我们作为参数传递一个回调函数给另一个函数时,我们只传递了这个函数的定义,并没有在参数中执行它。
当包含(调用)函数拥有了在参数中定义的回调函数后,它可以在任何时候调用(也就是回调)它。
这说明回调函数并不是立即执行,而是在包含函数的函数体内指定的位置“回调”它(形如其名)。所以,即使第一个jQuery的例子看起来是这样:
//Theanonymousfunctionisnotbeingexecutedthereintheparameter. //Theitemisacallbackfunction $("#btn_1").click(function(){ alert("Btn1Clicked"); });
匿名函数将延迟在click函数的函数体内被调用,即使没有名称,也可以被包含函数通过arguments对象访问。
回调函数是闭包的
当作为参数传递一个回调函数给另一个函数时,回调函数将在包含函数函数体内的某个位置被执行,就像回调函数在包含函数的函数体内定义一样。这意味着回调函数是闭包的,想更多地了解闭包,请参考作者另一个贴子UnderstandJavaScriptClosuresWithEase。从所周知,闭包函数可以访问包含函数的作用域,所以,回调函数可以访问包含函数的变量,甚至是全局变量。
三、实现回调函数的基本原则
简单地说,自己实现回调函数的时候需要遵循几条原则。
1、使用命名函数或匿名函数作为回调
在前面的jQuery和forEach的例子中,我们在包含函数的参数中定义匿名函数,这是使用回调函数的通用形式之一,另一个经常被使用的形式是定义一个带名称的函数,并将函数名作为参数传递给另一个函数,例如:
//globalvariable varallUserData=[]; //genericlogStufffunctionthatprintstoconsole functionlogStuff(userData){ if(typeofuserData==="string") { console.log(userData); } elseif(typeofuserData==="object") { for(variteminuserData){ console.log(item+":"+userData[item]); } } } //Afunctionthattakestwoparameters,thelastoneacallbackfunction functiongetInput(options,callback){ allUserData.push(options); callback(options); } //WhenwecallthegetInputfunction,wepasslogStuffasaparameter. //SologStuffwillbethefunctionthatwillcalledback(orexecuted)insidethegetInputfunction getInput({name:"Rich",speciality:"JavaScript"},logStuff); //name:Rich //speciality:JavaScript
2、传递参数给回调函数
因为回调函数在执行的时候就和一般函数一样,我们可以传递参数给它。可以将包含函数的任何属性(或全局的属性)作为参数传递回调函数。在上一个例子中,我们将包含函数的options作为参数传递给回调函数。下面的例子让我们传递一个全局变量或本地变量给回调函数:
//Globalvariable vargeneralLastName="Clinton"; functiongetInput(options,callback){ allUserData.push(options); //PasstheglobalvariablegeneralLastNametothecallbackfunction callback(generalLastName,options); }
3、在执行之前确保回调是一个函数
在调用之前,确保通过参数传递进来的回调是一个需要的函数通常是明智的。此外,让回调函数是可选的也是一个好的实践。
让我们重构一下上面例子中的getInput函数,确保回调函数做了适当的检查。
functiongetInput(options,callback){ allUserData.push(options); //Makesurethecallbackisafunction if(typeofcallback==="function"){ //Callit,sincewehaveconfirmeditiscallable callback(options); } }
如果getInput函数没有做适当的检查(检查callback是否是函数,或是否通过参数传递进来了),我们的代码将会导致运行时错误。
4、使用含有this对象的回调函数的问题
当回调函数是一个含有this对象的方法时,我们必须修改执行回调函数的方法以保护this对象的内容。否则this对象将会指向全局的window对象(如果回调函数传递给了全局函数),或指向包含函数。让我们看看下面的代码:
//Defineanobjectwithsomepropertiesandamethod //Wewilllaterpassthemethodasacallbackfunctiontoanotherfunction varclientData={ id:094545, fullName:"NotSet", //setUserNameisamethodontheclientDataobject setUserName:function(firstName,lastName){ //thisreferstothefullNamepropertyinthisobject this.fullName=firstName+""+lastName; } } functiongetUserInput(firstName,lastName,callback){ //DootherstufftovalidatefirstName/lastNamehere //Nowsavethenames callback(firstName,lastName); }
在下面的示例代码中,当clientData.setUserName被执行时,this.fullName不会设置clientData对象中的属性fullName,而是设置window对象中的fullName,因为getUserInput是一个全局函数。出现这种现象是因为在全局函数中this对象指向了window对象。
getUserInput("Barack","Obama",clientData.setUserName); console.log(clientData.fullName);//NotSet //ThefullNamepropertywasinitializedonthewindowobject console.log(window.fullName);//BarackObama
5、使用Call或Apply函数保护this对象
我们可以通过使用Call或Apply函数来解决前面示例中的问题。到目前为止,我们知道JavaScript中的每一个函数都有两个方法:Call和Apply。这些方法可以被用来在函数内部设置this对象的内容,并内容传递给函数参数指向的对象。
Calltakesthevaluetobeusedasthethisobjectinsidethefunctionasthefirstparameter,andtheremainingargumentstobepassedtothefunctionarepassedindividually(separatedbycommasofcourse).TheApplyfunction'sfirstparameterisalsothevaluetobeusedasthethisobjectinsidethefunction,whilethelastparameterisanarrayofvalues(ortheargumentsobject)topasstothefunction. (该段翻译起来太拗口了,放原文自己体会)
这听起来很复杂,但让我们看看Apply和Call的使用是多么容易。为解决前面例子中出现的问题,我们使用Apply函数如下:
//Notethatwehaveaddedanextraparameterforthecallbackobject,called"callbackObj" functiongetUserInput(firstName,lastName,callback,callbackObj){ //Dootherstufftovalidatenamehere //TheuseoftheApplyfunctionbelowwillsetthethisobjecttobecallbackObj callback.apply(callbackObj,[firstName,lastName]); }
通过Apply函数正确地设置this对象,现在我们可以正确地执行回调函数并它正确地设置clientData对象中的fullName属性。
//WepasstheclientData.setUserNamemethodandtheclientDataobjectasparameters.TheclientDataobjectwillbeusedbytheApplyfunctiontosetthethisobject getUserInput("Barack","Obama",clientData.setUserName,clientData); //thefullNamepropertyontheclientDatawascorrectlyset console.log(clientData.fullName);//BarackObama
我们也可以使用Call函数,但在本例中我们使用的Apply函数。
6、多重回调函数也是允许的
我们可以传递多个回调函数给另一个函数,就像传递多个变量一样。这是使用jQuery的AJAX函数的典型例子:
functionsuccessCallback(){ //Dostuffbeforesend } functionsuccessCallback(){ //Dostuffifsuccessmessagereceived } functioncompleteCallback(){ //Dostuffuponcompletion } functionerrorCallback(){ //Dostuffiferrorreceived } $.ajax({ url:"http://fiddle.jshell.net/favicon.png", success:successCallback, complete:completeCallback, error:errorCallback });
四、“回调地狱”的问题和解决方案
异步代码执行是一种简单的以任意顺序执行的方式,有时是很常见的有很多层级的回调函数,你看起来像下面这样的代码。下面这种凌乱的代码称作“回调地狱”,因为它是一种包含非常多的回调的麻烦的代码。我是在node-mongodb-native里看到这个例子的,MongoDB驱动Node.js.示例代码就像这样:
varp_client=newDb('integration_tests_20',newServer("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':newObjectID("aaaaaaaaaaaa")},function(err,cursor){ cursor.toArray(function(err,items){ test.assertEquals(1,items.length); //Let'sclosethedb p_client.close(); }); }); }); }); }); });
你不太可能在自己的代码里碰到这个的问题,但如果你碰到了(或以后偶然碰到了),那么有以下两种方式解决这个问题。
命名并定义你的函数,然后传递函数名作为回调,而不是在主函数的参数列表里定义一个匿名函数。
模块化:把你的代码划分成一个个模块,这样你可以空出一部分代码块做特殊的工作。然后你可以将这个模型引入到你的大型应用程序中。
五、实现自己的回调函数
现在你已经完全理解(我相信你已经理解了,如果没有请快速重新阅读一遍)了JavaScript关于回调的所用特性并且看到回调的使用是如此简单但功能却很强大。你应该看看自己的代码是否有机会使用回调函数,有以下需求时你可以考虑使用回调:
- 避免重复代码(DRY—DoNotRepeatYourself)
- 在你需要更多的通用功能的地方更好地实现抽象(可处理各种类型的函数)。
- 增强代码的可维护性
- 增强代码的可读性
- 有更多定制的功能
实现自己的回调函数很简单,在下面的例子中,我可以创建一个函数完成所用的工作:获取用户数据,使用用户数据生成一首通用的诗,使用用户数据来欢迎用户,但这个函数将会是一个凌乱的函数,到处是if/else的判断,甚至会有很多的限制并无法执行应用程序可能需要的处理用户数据的其它函数。
替而代之的是我让实现增加了回调函数,这样主函数获取用户数据后可以传递用户全名和性别给回调函数的参数并执行回调函数以完成任何任务。
简而言之,getUserInput函数是通用的,它可以执行多个拥有各种功能的回调函数。
//First,setupthegenericpoemcreatorfunction;itwillbethecallbackfunctioninthegetUserInputfunctionbelow. functiongenericPoemMaker(name,gender){ console.log(name+"isfinerthanfinewine."); console.log("Altruisticandnobleforthemoderntime."); console.log("Alwaysadmirablyadornedwiththelateststyle."); console.log("A"+gender+"ofunfortunatetragedieswhostillmanagesaperpetualsmile"); } //Thecallback,whichisthelastitemintheparameter,willbeourgenericPoemMakerfunctionwedefinedabove. functiongetUserInput(firstName,lastName,gender,callback){ varfullName=firstName+""+lastName; //Makesurethecallbackisafunction if(typeofcallback==="function"){ //Executethecallbackfunctionandpasstheparameterstoit callback(fullName,gender); } }
调用getUserInput函数并传递genericPoemMaker函数作为回调:
getUserInput("Michael","Fassbender","Man",genericPoemMaker); //Output /*MichaelFassbenderisfinerthanfinewine. Altruisticandnobleforthemoderntime. Alwaysadmirablyadornedwiththelateststyle. AManofunfortunatetragedieswhostillmanagesaperpetualsmile. */
因为getUserInput函数只处理用户数据的输入,我们可以传递任何回调函数给它。例如我们可以像这样传递一个greetUser函数。
functiongreetUser(customerName,sex){ varsalutation=sex&&sex==="Man"?"Mr.":"Ms."; console.log("Hello,"+salutation+""+customerName); } //PassthegreetUserfunctionasacallbacktogetUserInput getUserInput("Bill","Gates","Man",greetUser); //Andthisistheoutput Hello,Mr.BillGates
和上一个例子一样,我们调用了同一个getUserInput函数,但这次却执行了完全不同的任务。
如你所见,回调函数提供了广泛的功能。尽管前面提到的例子非常简单,在你开始使用回调函数的时候思考一下你可以节省多少工作,如何更好地抽象你的代码。加油吧!在早上起来时想一想,在晚上睡觉前想一想,在你休息时想一想……
我们在JavaScript中经常使用回调函数时注意以下几点,尤其是现在的web应用开发,在第三方库和框架中
- 异步执行(例如读文件,发送HTTP请求)
- 事件监听和处理
- 设置超时和时间间隔的方法
- 通用化:代码简洁
以上就是更加深入的学习了JavaScript的回调函数,希望对大家的学习有所帮助。