在JavaScrip中,function是內置的類對象,也就是說它是一種類型的對象,能夠和其它String、Array、Number、Object類的對象同樣用於內置對象的管理。由於function其實是一種對象,它能夠「存儲在變量中,經過參數傳遞給(別一個)函數(function),在函數內部建立,從函數中返回結果值」。
由於function是內置對象,咱們能夠將它做爲參數傳遞給另外一個函數,延遲到函數中執行,甚至執行後將它返回。這是在JavaScript中使用回調函數的精髓。本篇文章的剩餘部分將全面學習JavaScript的回調函數。回調函數也許是JavaScript中使用最普遍的功能性編程技術,也許僅僅一小段JavaScript或jQuery的代碼都會給開發者留下一種神祕感,閱讀這篇文章後,也許會幫你消除這種神祕感。
回調函數來自一種著名的編程範式——函數式編程,在基本層面上,函數式編程指定的了函數的參數。函數式編程雖然如今的使用範圍變小了,但它一直被「專業的聰明的」程序員看做是一種難懂的技術,之前是這樣,將來也將是如此。javascript
幸運的是,函數式編程已經被闡述的像你我這樣的通常人也能理解和使用。函數式編程最主要的技術之一就是回調函數,你很快會閱讀到,實現回調函數就像傳遞通常的參數變量同樣簡單。這項技術如此的簡單,以致於我都懷疑爲何它常常被包含在JavaScript的高級話題中去。java
什麼是回調或高級函數?
回調函數被認爲是一種高級函數,一種被做爲參數傳遞給另外一個函數(在這稱做"otherFunction")的高級函數,回調函數會在otherFunction內被調用(或執行)。回調函數的本質是一種模式(一種解決常見問題的模式),所以回調函數也被稱爲回調模式。node
思考一下下面這種在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中。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的參數。web
到目前爲止,咱們傳遞了一個匿名的函數做爲參數給另外一個函數或方法。在看其它更復雜的回調函數以前,讓咱們理解一下回調的工做原理並實現一個本身的回調函數。ajax
回調函數是如何實現的?
咱們能夠像使用變量同樣使用函數,做爲另外一個函數的參數,在另外一個函數中做爲返回結果,在另外一個函數中調用它。當咱們做爲參數傳遞一個回調函數給另外一個函數時,咱們只傳遞了這個函數的定義,並無在參數中執行它。mongodb
當包含(調用)函數擁有了在參數中定義的回調函數後,它能夠在任什麼時候候調用(也就是回調)它。shell
這說明回調函數並非當即執行,而是在包含函數的函數體內指定的位置「回調」它(形如其名)。因此,即便第一個jQuery的例子看起來是這樣:express
//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。從所周知,閉包函數能夠訪問包含函數的做用域,因此,回調函數能夠訪問包含函數的變量,甚至是全局變量。
實現回調函數的基本原則
簡單地說,本身實現回調函數的時候須要遵循幾條原則。
使用命名函數或匿名函數做爲回調
在前面的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
傳遞參數給回調函數
由於回調函數在執行的時候就和通常函數同樣,咱們能夠傳遞參數給它。能夠將包含函數的任何屬性(或全局的屬性)做爲參數傳遞迴調函數。在上一個例子中,咱們將包含函數的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);
}
在執行以前確保回調是一個函數
在調用以前,確保經過參數傳遞進來的回調是一個須要的函數一般是明智的。此外,讓回調函數是可選的也是一個好的實踐。
讓咱們重構一下上面例子中的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是不是函數,或是否經過參數傳遞進來了),咱們的代碼將會致使運行時錯誤。
使用含有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
使用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 函數。
多重回調函數也是容許的
咱們能夠傳遞多個回調函數給另外一個函數,就像傳遞多個變量同樣。這是使用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關於回調的所用特性而且看到回調的使用是如此簡單但功能卻很強大。你應該看看本身的代碼是否有機會使用回調函數,有如下需求時你能夠考慮使用回調:
- 避免重複代碼 (DRY—Do Not Repeat Yourself)
- 在你須要更多的通用功能的地方更好地實現抽象(可處理各類類型的函數)。
- 加強代碼的可維護性
- 加強代碼的可讀性
- 有更多定製的功能
實現本身的回調函數很簡單,在下面的例子中,我能夠建立一個函數完成所用的工做:獲取用戶數據,使用用戶數據生成一首通用的詩,使用用戶數據來歡迎用戶,但這個函數將會是一個凌亂的函數,處處是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應用開發,在第三方庫和框架中
- 異步執行(例如讀文件,發送HTTP請求)
- 事件監聽和處理
- 設置超時和時間間隔的方法
- 通用化:代碼簡潔