瞭解JavaScript中的回撥函數並使用它們

2020-10-19 21:00:17

在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物件的上下文。否則如果回撥函數被傳遞給一個全域性函數,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函數來儲存this

我們可以使用Call或者Apply函數來修復上面你的問題。到目前為止,我們知道了每個JavaScript中的函數都有兩個方法:CallApply。這些方法被用來設定函數內部的this物件以及給此函數傳遞變數。

call接收的第一個引數為被用來在函數內部當做this的物件,傳遞給函數的引數被挨個傳遞(當然使用逗號分開)。Apply函數的第一個引數也是在函數內部作為this的物件,然而最後一個引數確是傳遞給函數的值的陣列。

聽起來很複雜,那麼我們來看看使用ApplyCall有多麼的簡單。為了修復前面例子的問題,我將在下面你的例子中使用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請求,等等)

  • 時間監聽器/處理器

  • setTimeoutsetInterval方法

  • 一般情況:精簡程式碼

結束語

JavaScript回撥函數非常美妙且功能強大,它們為你的Web應用和程式碼提供了諸多好處。你應該在有需求時使用它;或者為了程式碼的抽象性,可維護性以及可讀性而使用回撥函數來重構你的程式碼。

相關免費學習推薦:

以上就是了解JavaScript中的回撥函數並使用它們的詳細內容,更多請關注TW511.COM其它相關文章!