本文的目錄:
在JavaScrip中,function是內置的類對象,也就是說它是一種類型的對象,可以和其它String、Array、Number、Object類的對象一樣用於內置對象的管理。因為function實際上是一種對象,它可以“存儲在變量中,通過參數傳遞給(別一個)函數(function),在函數內部創建,從函數中返回結果值”。
因為function是內置對象,我們可以將它作為參數傳遞給另一個函數,延遲到函數中執行,甚至執行後將它返回。這是在JavaScript中使用回調函數的精髓。本篇文章的剩余部分將全面學習JavaScript的回調函數。回調函數也許是JavaScript中使用最廣泛的功能性編程技術,也許僅僅一小段JavaScript或jQuery的代碼都會給開發者留下一種神秘感,閱讀這篇文章後,也許會幫你消除這種神秘感。
回調函數來自一種著名的編程范式——函數式編程,在基本層面上,函數式編程指定的了函數的參數。函數式編程雖然現在的使用范圍變小了,但它一直被“專業的聰明的”程序員看作是一種難懂的技術,以前是這樣,未來也將是如此。
幸運的是,函數式編程已經被闡述的像你我這樣的一般人也能理解和使用。函數式編程最主要的技術之一就是回調函數,你很快會閱讀到,實現回調函數就像傳遞一般的參數變量一樣簡單。這項技術如此的簡單,以至於我都懷疑為什麼它經常被包含在JavaScript的高級話題中去。
一、什麼是回調或高級函數?
回調函數被認為是一種高級函數,一種被作為參數傳遞給另一個函數(在這稱作"otherFunction")的高級函數,回調函數會在otherFunction內被調用(或執行)。回調函數的本質是一種模式(一種解決常見問題的模式),因此回調函數也被稱為回調模式。
思考一下下面這種在jQuery中常用的回調函數:
//Note that the item in the click method's parameter is a function, not a variable. //The item is a callback function $("#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方法,作為forEach的參數。
到目前為止,我們傳遞了一個匿名的函數作為參數給另一個函數或方法。在看其它更復雜的回調函數之前,讓我們理解一下回調的工作原理並實現一個自己的回調函數。
二、回調函數是如何實現的?
我們可以像使用變量一樣使用函數,作為另一個函數的參數,在另一個函數中作為返回結果,在另一個函數中調用它。當我們作為參數傳遞一個回調函數給另一個函數時,我們只傳遞了這個函數的定義,並沒有在參數中執行它。
當包含(調用)函數擁有了在參數中定義的回調函數後,它可以在任何時候調用(也就是回調)它。
這說明回調函數並不是立即執行,而是在包含函數的函數體內指定的位置“回調”它(形如其名)。所以,即使第一個jQuery的例子看起來是這樣:
//The anonymous function is not being executed there in the parameter. //The item is a callback function $("#btn_1").click(function() { alert("Btn 1 Clicked"); });
匿名函數將延遲在click函數的函數體內被調用,即使沒有名稱,也可以被包含函數通過 arguments對象訪問。
回調函數是閉包的
當作為參數傳遞一個回調函數給另一個函數時,回調函數將在包含函數函數體內的某個位置被執行,就像回調函數在包含函數的函數體內定義一樣。這意味著回調函數是閉包的,想更多地了解閉包,請參考作者另一個貼子Understand JavaScript Closures With Ease。從所周知,閉包函數可以訪問包含函數的作用域,所以,回調函數可以訪問包含函數的變量,甚至是全局變量。
三、實現回調函數的基本原則
簡單地說,自己實現回調函數的時候需要遵循幾條原則。
1、使用命名函數或匿名函數作為回調
在前面的jQuery和forEach的例子中,我們在包含函數的參數中定義匿名函數,這是使用回調函數的通用形式之一,另一個經常被使用的形式是定義一個帶名稱的函數,並將函數名作為參數傳遞給另一個函數,例如:
// global variable var allUserData = []; // generic logStuff function that prints to console 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]); } } } // A function that takes two parameters, the last one a callback function function getInput (options, callback) { allUserData.push (options); callback (options); } // When we call the getInput function, we pass logStuff as a parameter. // So logStuff will be the function that will called back (or executed) inside the getInput function getInput ({name:"Rich", speciality:"JavaScript"}, logStuff); // name: Rich // speciality: JavaScript
2、傳遞參數給回調函數
因為回調函數在執行的時候就和一般函數一樣,我們可以傳遞參數給它。可以將包含函數的任何屬性(或全局的屬性)作為參數傳遞回調函數。在上一個例子中,我們將包含函數的options作為參數傳遞給回調函數。下面的例子讓我們傳遞一個全局變量或本地變量給回調函數:
//Global variable var generalLastName = "Clinton"; function getInput (options, callback) { allUserData.push (options); // Pass the global variable generalLastName to the callback function callback (generalLastName, options); }
3、在執行之前確保回調是一個函數
在調用之前,確保通過參數傳遞進來的回調是一個需要的函數通常是明智的。此外,讓回調函數是可選的也是一個好的實踐。
讓我們重構一下上面例子中的getInput函數,確保回調函數做了適當的檢查。
function getInput(options, callback) { allUserData.push(options); // Make sure the callback is a function if (typeof callback === "function") { // Call it, since we have confirmed it is callable callback(options); } }
如果getInput函數沒有做適當的檢查(檢查callback是否是函數,或是否通過參數傳遞進來了),我們的代碼將會導致運行時錯誤。
4、使用含有this對象的回調函數的問題
當回調函數是一個含有this對象的方法時,我們必須修改執行回調函數的方法以保護this對象的內容。否則this對象將會指向全局的window對象(如果回調函數傳遞給了全局函數),或指向包含函數。讓我們看看下面的代碼:
// Define an object with some properties and a method // We will later pass the method as a callback function to another function var clientData = { id: 094545, fullName: "Not Set", // setUserName is a method on the clientData object setUserName: function (firstName, lastName) { // this refers to the fullName property in this object this.fullName = firstName + " " + lastName; } } function getUserInput(firstName, lastName, callback) { // Do other stuff to validate firstName/lastName here // Now save the 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 // The fullName property was initialized on the window object console.log (window.fullName); // Barack Obama
5、使用Call或Apply函數保護this對象
我們可以通過使用 Call 或 Apply函數來解決前面示例中的問題。到目前為止,我們知道JavaScript中的每一個函數都有兩個方法:Call和Apply。這些方法可以被用來在函數內部設置this對象的內容,並內容傳遞給函數參數指向的對象。
Call takes the value to be used as the this object inside the function as the first parameter, and the remaining arguments to be passed to the function are passed individually (separated by commas of course). The Apply function's first parameter is also the value to be used as the thisobject inside the function, while the last parameter is an array of values (or the arguments object) to pass to the function. (該段翻譯起來太拗口了,放原文自己體會)
這聽起來很復雜,但讓我們看看Apply和Call的使用是多麼容易。為解決前面例子中出現的問題,我們使用Apply函數如下:
//Note that we have added an extra parameter for the callback object, called "callbackObj" function getUserInput(firstName, lastName, callback, callbackObj) { // Do other stuff to validate name here // The use of the Apply function below will set the this object to be callbackObj callback.apply (callbackObj, [firstName, lastName]); }
通過Apply函數正確地設置this對象,現在我們可以正確地執行回調函數並它正確地設置clientData對象中的fullName屬性。
// We pass the clientData.setUserName method and the clientData object as parameters. The clientData object will be used by the Apply function to set the this object getUserInput ("Barack", "Obama", clientData.setUserName, clientData); // the fullName property on the clientData was correctly set console.log (clientData.fullName); // Barack Obama
我們也可以使用Call 函數,但在本例中我們使用的Apply 函數。
6、多重回調函數也是允許的
我們可以傳遞多個回調函數給另一個函數,就像傳遞多個變量一樣。這是使用jQuery的AJAX函數的典型例子:
function successCallback() { // Do stuff before send } function successCallback() { // Do stuff if success message received } function completeCallback() { // Do stuff upon completion } function errorCallback() { // Do stuff if error received } $.ajax({ url:"http://fiddle.jshell.net/favicon.png", success:successCallback, complete:completeCallback, error:errorCallback });
四、“回調地獄”的問題和解決方案
異步代碼執行是一種簡單的以任意順序執行的方式,有時是很常見的有很多層級的回調函數,你看起來像下面這樣的代碼。下面這種凌亂的代碼稱作“回調地獄”,因為它是一種包含非常多的回調的麻煩的代碼。我是在node-mongodb-native裡看到這個例子的,MongoDB驅動Node.js.示例代碼就像這樣:
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(); }); }); }); }); }); });
你不太可能在自己的代碼裡碰到這個的問題,但如果你碰到了(或以後偶然碰到了),那麼有以下兩種方式解決這個問題。
命名並定義你的函數,然後傳遞函數名作為回調,而不是在主函數的參數列表裡定義一個匿名函數。
模塊化:把你的代碼劃分成一個個模塊,這樣你可以空出一部分代碼塊做特殊的工作。然後你可以將這個模型引入到你的大型應用程序中。
五、實現自己的回調函數
現在你已經完全理解(我相信你已經理解了,如果沒有請快速重新閱讀一遍)了JavaScript關於回調的所用特性並且看到回調的使用是如此簡單但功能卻很強大。你應該看看自己的代碼是否有機會使用回調函數,有以下需求時你可以考慮使用回調:
實現自己的回調函數很簡單,在下面的例子中,我可以創建一個函數完成所用的工作:獲取用戶數據,使用用戶數據生成一首通用的詩,使用用戶數據來歡迎用戶,但這個函數將會是一個凌亂的函數,到處是if/else的判斷,甚至會有很多的限制並無法執行應用程序可能需要的處理用戶數據的其它函數。
替而代之的是我讓實現增加了回調函數,這樣主函數獲取用戶數據後可以傳遞用戶全名和性別給回調函數的參數並執行回調函數以完成任何任務。
簡而言之,getUserInput函數是通用的,它可以執行多個擁有各種功能的回調函數。
// First, setup the generic poem creator function; it will be the callback function in the getUserInput function below. 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"); } //The callback, which is the last item in the parameter, will be our genericPoemMaker function we defined above. 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); // Output /* 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); } // Pass the greetUser function as a callback to getUserInput getUserInput("Bill", "Gates", "Man", greetUser); // And this is the output Hello, Mr. Bill Gates
和上一個例子一樣,我們調用了同一個getUserInput 函數,但這次卻執行了完全不同的任務。
如你所見,回調函數提供了廣泛的功能。盡管前面提到的例子非常簡單,在你開始使用回調函數的時候思考一下你可以節省多少工作,如何更好地抽象你的代碼。加油吧!在早上起來時想一想,在晚上睡覺前想一想,在你休息時想一想……
我們在JavaScript中經常使用回調函數時注意以下幾點,尤其是現在的web應用開發,在第三方庫和框架中
以上就是更加深入的學習了JavaScript的回調函數,希望對大家的學習有所幫助。