在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", "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
对象要么指向全局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","Obama",clientData.setUserName); console.log(clientData,fullName); //Not Set //fullName属性将在window对象中被初始化 console.log(window.fullName); //Barack Obama
我们可以使用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(); }); }); }); }); }); });
你应该不想在你的代码中遇到这样的问题,当你当你遇到了
你将会时不时的遇到这种情况
这里有关于这个问题的两种解决方案。
给你的函数命名并传递它们的名字作为回调函数,而不是主函数的参数中定义匿名函数。
模块化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应用和代码提供了诸多好处。你应该在有需求时使用它;或者为了代码的抽象性,可维护性以及可读性而使用回调函数来重构你的代码。
推荐教程:《JavaScript视频教程》
以上是詳解JavaScript中的回呼函數並使用它的詳細內容。更多資訊請關注PHP中文網其他相關文章!