搜索
您的当前位置:首页正文

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

来源:六九路网
理解与使⽤JavaScript中的回调函数

在JavaScript中,函数是第⼀类对象,这意味着函数可以像对象⼀样按照第⼀类管理被使⽤。既然函数实际上是对象:它们能被“存储”在变量中,能作为函数参数被传递,能在函数中被创建,能从函数中返回。

因为函数是第⼀类对象,我们可以在JavaScript使⽤回调函数。在下⾯的⽂章中,我们将学到关于回调函数的⽅⽅⾯⾯。回调函数可能是在JavaScript中使⽤最多的函数式编程技巧,虽然在字⾯上看起来它们⼀直⼀⼩段JavaScript或者jQuery代码,但是对于许多开发者来说它任然是⼀个谜。在阅读本⽂之后你能了解怎样使⽤回调函数。

回调函数是从⼀个叫函数式编程的编程范式中衍⽣出来的概念。简单来说,函数式编程就是使⽤函数作为变量。函数式编程过去 - 甚⾄是现在,依旧没有被⼴泛使⽤ - 它过去常被看做是那些受过特许训练的,⼤师级别的程序员的秘传技巧。

幸运的是,函数是编程的技巧现在已经被充分阐明因此像我和你这样的普通⼈也能去轻松使⽤它。函数式编程中的⼀个主要技巧就是回调函数。在后⾯内容中你会发现实现回调函数其实就和普通函数传参⼀样简单。这个技巧是如此的简单以致于我常常感到很奇怪为什么它经常被包含在讲述JavaScript⾼级技巧的章节中。

什么是回调或者⾼阶函数

⼀个回调函数,也被称为⾼阶函数,是⼀个被作为参数传递给另⼀个函数(在这⾥我们把另⼀个函数叫做otherFunction)的函数,回调函数在otherFunction中被调⽤。⼀个回调函数本质上是⼀种编程模式(为⼀个常见问题创建的解决⽅案),因此,使⽤回调函数也叫做回调模式。

下⾯是⼀个在jQuery中使⽤回调函数简单普遍的例⼦:

//注意到click⽅法中是⼀个函数⽽不是⼀个变量//它就是回调函数

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

正如你在前⾯的例⼦中看到的,我们将⼀个函数作为参数传递给了click⽅法。click⽅法会调⽤(或者执⾏)我们传递给它的函数。这是JavaScript中回调函数的典型⽤法,它在jQuery中⼴泛被使⽤。下⾯是另⼀个JavaScript中典型的回调函数的例⼦:

var friends = [\"Mike\

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\//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: fucntion (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\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\//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\ {'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(); }); }); }); }); });});

你应该不想在你的代码中遇到这样的问题,当你当你遇到了

你将会时不时的遇到这种情况

这⾥有关于这个问题的两种解决⽅案。

给你的函数命名并传递它们的名字作为回调函数,⽽不是主函数的参数中定义匿名函数。

模块化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\// 输出

/* 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\// 这⾥是输出

Hello, Mr. Bill Gates

我们调⽤了完全相同的getUserInput函数,但是这次完成了⼀个完全不同的任务。

正如你所见,回调函数很神奇。即使前⾯的例⼦相对简单,想象⼀下能节省多少⼯作量,你的代码将会变得更加的抽象,这⼀切只需要你开始使⽤毁掉函数。⼤胆的去使⽤吧。

在JavaScript编程中回调函数经常以⼏种⽅式被使⽤,尤其是在现代Web应⽤开发以及库和框架中:

异步调⽤(例如读取⽂件,进⾏HTTP请求,等等)时间监听器/处理器

setTimeout和setInterval⽅法⼀般情况:精简代码

结束语

JavaScript回调函数⾮常美妙且功能强⼤,它们为你的Web应⽤和代码提供了诸多好处。你应该在有需求时使⽤它;或者为了代码的抽象性,可维护性以及可读性⽽使⽤回调函数来重构你的代码。

因篇幅问题不能全部显示,请点此查看更多更全内容

Top