理解 JavaScript 回調函數並使用

JavaScript中,函數是一等(first-class)對象;也就是說,函數是 Object 類型並且可以像其他一等對象(String,Array,Number等)一樣使用。它們可以“保存在變量中,作爲參數傳遞給函數,在函數內創建,以及被函數返回”。

由於函數是一等對象,我們可以把一個函數作爲參數傳遞給另一個函數,然後在那個函數內執行,甚至也可以被那個函數返回,然後再執行。這就是 JavaScript 中回調函數(callback functions)的本質。在本文的剩餘部分,我們將學習到關於 JavaScript 回調函數的所有知識。回調函數可能是 JavaScript 中使用最廣泛的函數式編程技術了,你可以在任何一段 JavaScript或jQuery 代碼發現它,但是,它對很多 JavaScript 開發者來說依然是神祕的。直到你閱讀了本文,就再也不會對它感到神祕了。

回調函數 是來源於函數式編程的一種技術。從底層來說,函數式編程把函數用作參數。函數式編程過去是 —— 現在仍然是,(儘管如今不太流行)被有經驗的、高級開發者視作難懂的技術。

幸運的是,函數式編程已經被闡明到像我們這樣的的普通人都能容易理解的地步。其主要技術之一就是回調函數。下面你就會看到,實現回調函數很容易,就像傳遞一個普通變量參數一樣。這個技術如此簡單以至於我很奇怪爲什麼大多數教程都把它歸類爲高級主題裏面。

回調是什麼?

回調函數,也叫高階(higher-order)函數,是一個作爲參數傳遞到其他函數的函數,然後回調函數在其他函數中被調用。回調函數本質上是一個模式,因此使用回調函數被稱爲回調模式。

請看下面的代碼,這是 jQuery 中常見的回調函數的使用:

$("#btn_1").click(function(){
    alert("Btn 1 Clicked");
});

這裏,傳遞了一個匿名函數給 click 方法。click 方法將調用或執行這個回調函數。

再看一個例子:

var friend = ["Mike", "Stacy", "Andy", "Rick"];

friend.forEach(function(eachName, index){
    console.log(index + 1 + "." + eachName); // 1.Mike,2.Stacy,3.Andy,4.Rick
});

這裏,傳遞了一個匿名函數給 forEach 方法。

回調函數如何工作?

當我們傳遞一個回調函數給其他函數時,我們只是傳遞了函數定義。我們並沒有在參數中執行函數。換句話說,傳遞函數時不能在函數名後面加括號“()”,而執行函數時那樣需要。

由於其他函數在參數中有該回調函數的定義,所以它可以在任何時候執行該函數。

注意到回調函數不是立即執行的,所謂“回調”就是指在其他函數中某個特定的時候被回頭調用。所以再看第一個例子,click 函數中的匿名函數將在click函數體內被調用。即使該函數匿名,也可以通過 arguments 對象訪問到。

回調函數都是閉包
當傳遞一個回調函數給其他函數時,回調函數在其他函數函數體內某處執行,就好像回調函數是在其他函數中定義的。這說明回調函數是一個閉包(closure)。衆所周知,閉包可以訪問外層包含函數的作用域,所以回調函數也能訪問其他函數的變量,甚至全局作用域中的變量。

實現回調函數所遵循的基本準則

儘管不復雜,但仍然有一些值得注意的地方。

使用命名的或匿名的函數作爲回調函數

第二個例子中我們使用了匿名函數作爲回調函數,這是一種常見的做法。另一種常見做法是定義一個有名字的函數,然後傳遞給另一個函數。

/ 全局變量
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, calllback){
    allUserData.push(options);
    calllback(options);
}

// 調用 getInput 函數時傳遞了 logStuff,所以,logStuff 將在 getInput 函數中被調用(或執行)
getInput({name:"Rich",speciality:"JavaScript"},logStuff);
// name:Rich
// speciality:JavaScript

傳遞參數給回調函數
由於回調函數在執行時也只是一個普通函數,所以我們可以傳遞參數給它。可以傳遞外層函數的屬性或者全局變量。上例中,傳遞了 options 作爲回調函數的參數。然後,讓我們傳遞一個全局變量和局部變量。

// 全局變量
var generalLastName = "Clinton";

function getInput(options, calllback){
    allUserData.push(options);
    // 傳遞全局變量
    calllback(generalLastName, options);
}

執行前確保回調是一個函數

在調用之前檢查所傳入的回調函數是否真的是一個函數是一個好習慣。讓我們重構一下上例中getInput函數:

function getInput(options, calllback){
    allUserData.push(options);

    // 確保 calllback 是一個函數
    if(typeof calllback === "function"){
        // 調用之,因爲已經確保它是函數了
        calllback(options);
    }
}

如果不檢測其類型,當傳入的參數不是函數時,就會導致運行時錯誤。

回調函數與this相關的問題

當回調函數中用到了 this 對象時,我們不得不改變執行回調函數的方式來保持原有的 this 對象。否則,this 對象可能指向全局的 window 對象,如果回調函數被傳入了全局函數中的話。或者指向外層包含方法的對象。
下面在代碼中演示:

var clientData = {
    id: 012334,
    fullName: "Not Set",
    // setUserName 是 clientData 對象的方法
    setUserName: function(firstName, lastName){
        this.fullName = firstName + " " + lastName;
    }
}

function getUserInput(firstName, lastName, calllback){
    calllback(firstName,lastName);
}

下面在代碼中演示:
下面的代碼中,當 clientData.setUserName 執行時,this.fullName 將不會設置 clientData 對象的 fullName 屬性,而是設置爲 window 對象的 fullName 屬性。這是因爲全局函數中的 this 對象指向 window 對象

getUserInput("Barack", "Obama", clientData.setUserName);

console.log(clientData.fullName); // Not Set

console.log(window.fullName); // Barack Obama

使用 Call 或 Apply 函數來保持 this

我們可以通過 Call 或 Apply 函數來解決上面的問題。目前來說,JavaScript 中的每個函數都有兩個方法:Call 和 Apply。這兩個方法用來設置函數內的 this 對象。

Call 把第一個參數作爲函數內的 this 對象,其他的參數分別傳遞給函數。Apply 也是把第一個參數作爲函數內的 this 對象,而第二個參數是一個數組(或者argument對象)。

我們在下面的代碼中使用 Apply 來解決這個問題:

function getUserInput(firstName, lastName, calllback, calllbackObj){
    calllback.apply(calllbackObj, [firstName,lastName]);
}

apply 正確設置了 this 對象,現在可以正確執行回調,並設置 clientData 上的 fullName 屬性了。

getUserInput("Barack", "Obama", clientData.setUserName, clientData);

console.log(clientData.fullName); // Barack Obama

允許使用多個回調函數

我們傳遞多個回調函數到另外函數,就像傳遞多個變量一樣,下面時一個經典的jQuery AJAX函數的例子:

function successCallBack(){

}

function failCallBack(){

}

function completeCallback(){

}

function errorCallback(){

}

$.ajax({
    url:"http://www.91ymb.com/favicon.png",
    success: successCallBack,
    complete: completeCallback,
    error: errorCallback
});

“回調地獄”問題和解決

在異步代碼執行中,代碼可能以任何順序執行,有時會看到有很多層回調函數,比如下例:

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)
實現更好的抽象。
更好的可維護性
更好的可讀性
更多專門的函數

寫自己的回調函數也很簡單。下面的例子中,我將創建一個函數用來:取回用戶數據,使用數據生成詩句,然後告訴用戶。這聽起來好像是一個雜亂的函數,有很多if/else語句,並且可能被限制而不能用用戶數據做些其他的事情。

但是,我把具體功能的實現交給回調函數,這樣主函數用於取回用戶數據,然後簡單地傳遞用戶全名和性別給回調函數,然後執行回調函數就行了。

簡言之,getUserInput 函數是通用的:它執行所有回調函數來實現具體的功能。

// 首先,建立一個詩句生成函數,它將作爲回調函數傳遞
function genericPoemMaker(name, gender){
    console.log(name + " is finer than fine wine.");
    console.log("ALltrustic 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");
}

// 
function getUserInput(firstName, lastName, gender, callback){
    var fullName = firstName + " " + lastName;

    // 確保 callback 是函數
    if(typeof callback === "function"){
        callback(fullName, callback);
    }
}

// 調用 getUserInput,並傳遞迴調函數
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 函數:

function greetUser(customerName, sex){
    var salutation = sex & sex == "Man" ? "Mr." : "Ms.";
    console.log("Hello, " + salutation + " " + customerName);
}

getUserInput("Bill", "Gates", "Man", greetUser);

// 輸出
// Hello, Mr. Bill Gates

我們同樣調用 getUserInput 函數兩次,但執行了不同的任務。

注意到,下列場景是我們頻繁使用到回調函數的地方,尤其是現代 web 應用開發,庫和框架開發:

  1. 異步執行(如讀取文件,HTTP請求)
  2. 事件監聽器/處理器
  3. setTimeout 函數和 setInterval 函數
  4. 通用原則:代碼簡潔性

最後

JavaScript 回調函數很好用,有很多好處。現在就開始使用回調函數來重構代碼以提高抽象、可維護性、可讀性吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章