理解javascript中的回撥函式(callback)【轉】

Franson發表於2017-06-25
在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。從所周知,閉包函式可以訪問包含函式的作用域,所以,回撥函式可以訪問包含函式的變數,甚至是全域性變數。

實現回撥函式的基本原則

簡單地說,自己實現回撥函式的時候需要遵循幾條原則。

使用命名函式或匿名函式作為回撥
在前面的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();
                    });
                });
            });
        });
    });
});

你不太可能在自己的程式碼裡碰到這個的問題,但如果你碰到了(或以後偶然碰到了),那麼有以下兩種方式解決這個問題。

  1. 命名並定義你的函式,然後傳遞函式名作為回撥,而不是在主函式的引數列表裡定義一個匿名函式。
  2. 模組化:把你的程式碼劃分成一個個模組,這樣你可以空出一部分程式碼塊做特殊的工作。然後你可以將這個模型引入到你的大型應用程式中。

    



實現自己的回撥函式

現在你已經完全理解(我相信你已經理解了,如果沒有請快速重新閱讀一遍)了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請求)
  • 事件監聽和處理
  • 設定超時和時間間隔的方法
  • 通用化:程式碼簡潔 

 

這篇文章主要介紹了理解javascript中的回撥函式(callback),本文著重於對回撥函式概念的理解,需要的朋友可以參考下

 

最近在看 express,滿眼看去,到處是以函式作為引數的回撥函式的使用。如果這個概念理解不了,nodejs、express 的程式碼就會看得一塌糊塗。比如:

複製程式碼程式碼如下:

app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

app是物件,use是方法,方法的引數是一個帶參的匿名函式,函式體直接在後面給出了。這段程式碼怎麼理解呢?我們先來了解回撥函式這個概念。
首先要了解,在 js 中,函式也是物件,可以賦值給變數,可以作為引數放在函式的引數列表中。比如:
複製程式碼程式碼如下:

var doSomething = function(a,b)
{
 return a + b;
}

這段程式碼的意思是定義一個匿名函式,這個匿名函式除了沒有名字之外,其他跟普通的函式沒有什麼兩樣。然後把匿名函式賦值給變數doSomething。接下來我們呼叫:
複製程式碼程式碼如下:

console.log(doSomething(2,3));

 

這樣會輸出5。

回撥函式,就是放在另外一個函式(如 parent)的引數列表中,作為引數傳遞給這個 parent,然後在 parent 函式體的某個位置執行。說來抽象,看例子:

複製程式碼程式碼如下:

// To illustrate the concept of callback
var doit = function(callback)
{
    var a = 1,
        b = 2,
        c = 3;
    var t = callback(a,b,c);
    return t + 10;
};
var d = doit(function(x,y,z){
    return (x+y+z);
});
console.log(d);

先定義 doit 函式,有一個引數 callback。這個 callback 就是回撥函式,名字可以任意取。看函式體,先定義三個變數 a,b,c。然後呼叫 callback 函式。最後返回一個值。

 

下面就呼叫 doit 函式了。要注意的是,剛才定義 doit 時,callback 並沒有定義,所以剛才並不知道 callback 是幹什麼用的。這其實很好理解,我們平時定義函式的時候,引數也只是給出了一個名字,比如 a,在函式體中使用 a,但整個過程也並不知道 a 到底是什麼,只有在呼叫那個函式的時候才指定 a 的具體值,比如2.回過頭來,在呼叫 doit 的時候,我們就需要指定 callback 究竟是個什麼東西了。可以看到,這個函式完成了一個 sum 功能。

上述程式碼的執行過程是:

呼叫 doit函式,引數是一個匿名函式;進入 doit 的函式體中,先定義 a,b,c,然後執行剛才的匿名函式,引數是 a,b,c,並返回一個 t,最後返回一個 t+10給 d。

回到最初的例子,app.use(...)是函式呼叫。我們可以想象,之前一定定義了一個 use 方法,只是這裡沒有給出。這兩個例子一對比,就可以馬上理解了。

在使用nodejs、express 的時候,不可能每個方法或函式我們都要找到它的函式定義去看一看。所以只要知道那個定義裡面給 callback 傳遞了什麼引數就行了。然後在呼叫方法或函式時,在引數裡我們自己定義匿名函式來完成某些功能。

Over!

 

 


Javascript中的Callback方法淺析

投稿:junjie 字型:[增加 減小] 型別:轉載 時間:2015-03-15 我要評論

這篇文章主要介紹了Javascript中的Callback方法淺析,本文講解了什麼是callback、Javscript Callback、Callback是什麼、Callback例項等內容,需要的朋友可以參考下
 

什麼是callback


 回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用為呼叫它所指向的函式時,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。

 

這個解釋看上去很複雜,於是找到了知乎上一個更好的解釋

 


 你到一個商店買東西,剛好你要的東西沒有貨,於是你在店員那裡留下了你的電話,過了幾天店裡有貨了,店員就打了你的電話,然後你接到電話後就到店裡去取了貨。在這個例子裡,你的電話號碼就叫回撥函式,你把電話留給店員就叫登記回撥函式,店裡後來有貨了叫做觸發了回撥關聯的事件,店員給你打電話叫做呼叫回撥函式,你到店裡去取貨叫做響應回撥事件。回答完畢。

 

在Javascript中:

 


 函式A作為引數(函式引用)傳遞到另一個函式B中,並且這個函式B執行函式A。我們就說函式A叫做回撥函式。如果沒有名稱(函式表示式),就叫做匿名回撥函式。
實際上,也就是把函式作為引數傳遞。

 

Javscript Callback

把上面那些複雜的解釋都丟到垃圾桶裡吧~,看看Callback是什麼

Callback是什麼

在jQuery中, hide的方法大概是這樣子的


$(selector).hide(speed,callback)



$('#element').hide(1000, function() {
    // callback function
});

我們只需要在裡面寫一個簡單的函式
複製程式碼程式碼如下:

$('#element').hide(1000, function() {
    console.log('Hide');
});

有一個小小的註釋在這其中:Callback 函式在當前動畫 100% 完成之後執行。然後我們就可以看到真正的現象,當id為element的元素隱藏後,會在console中輸出Hide。

 

就也就意味著:

Callback實際上是,當一個函式執行完後,現執行的那個函式就是所謂的callback函式。

Callback作用

正常情況下函式都是按順序執行的,然而Javascript是一個事件驅動的語言。


function hello(){
    console.log('hello');
}

 

function world(){
    console.log('world');
}

hello();
world();


所以正常情況下都會按順序執行的,然而當執行world事件的時間比較長時。

function hello(){
    setTimeout( function(){
        console.log( 'hello' );
    }, 1000 );
}

 

function world(){
    console.log('world');
}

hello();
world();


那麼這個時候就不是這樣的,這時會輸出world,再輸出hello,故而我們需要callback。

 

Callback例項

一個簡單地例子如下

定義:
function add_callback(p1, p2 ,callback) {
    var my_number = p1 + p2;
    callback(my_number);
}

 呼叫:

add_callback(5, 15, function(num){
    console.log("call " + num);
});


在例子中我們有一個add_callback的函式,接收三個引數:前兩個是要相加的兩個引數,第三個引數是回撥函式。當函式執行時,返回相加結果,並在控制檯中輸出'call 20'。

相關文章