JS回調函數(深刻篇)

<有些錯別字>javascript

 

在Javascript中,函數是第一類對象,這意味着函數能夠像對象同樣按照第一類管理被使用。既然函數其實是對象:它們能被「存儲」在變量中,能做爲函數參數被傳遞,能在函數中被建立,能從函數中返回。html

由於函數是第一類對象,咱們能夠在Javascript使用回調函數。在下面的文章中,咱們將學到關於回調函數的方方面面。回調函數多是在Javascript中使用最多的函數式編程技巧,雖然在字面上看起來它們一直一小段Javascript或者jQuery代碼,可是對於許多開發者來講它任然是一個謎。在閱讀本文以後你能瞭解怎樣使用回調函數。java

回調函數是從一個叫函數式編程的編程範式中衍生出來的概念。簡單來講,函數式編程就是使用函數做爲變量。函數式編程過去 - 甚至是如今,依舊沒有被普遍使用 - 它過去常被看作是那些受過特許訓練的,大師級別的程序員的祕傳技巧。node

幸運的是,函數是編程的技巧如今已經被充分闡明所以像我和你這樣的普通人也能去輕鬆使用它。函數式編程中的一個主要技巧就是回調函數。在後面內容中你會發現實現回調函數其實就和普通函數傳參同樣簡單。這個技巧是如此的簡單以至於我經常感到很奇怪爲何它常常被包含在講述Javascript高級技巧的章節中。程序員

什麼是回調或者高階函數

一個回調函數,也被稱爲高階函數,是一個被做爲參數傳遞給另外一個函數(在這裏咱們把另外一個函數叫作「otherFunction」)的函數,回調函數在otherFunction中被調用。一個回調函數本質上是一種編程模式(爲一個常見問題建立的解決方案),所以,使用回調函數也叫作回調模式。web

下面是一個在jQuery中使用回調函數簡單廣泛的例子:ajax

/注意到click方法中是一個函數而不是一個變量
//它就是回調函數
$("#btn_1").click(function() {
  alert("Btn 1 Clicked");
});    

  

正如你在前面的例子中看到的,咱們將一個函數做爲參數傳遞給了click方法。click方法會調用(或者執行)咱們傳遞給它的函數。這是Javascript中回調函數的典型用法,它在jQuery中普遍被使用。mongodb

下面是另外一個Javascript中典型的回調函數的例子:shell

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的例子以下所示:

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

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

  

這個匿名函數稍後會在函數體內被調用。即便有名字,它依然在包含它的函數內經過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中的函數都有兩個方法:Call 和 Apply。這些方法被用來設置函數內部的this對象以及給此函數傳遞變量。

call接收的第一個參數爲被用來在函數內部當作this的對象,傳遞給函數的參數被挨個傳遞(固然使用逗號分開)。Apply函數的第一個參數也是在函數內部做爲this的對象,然而最後一個參數確是傳遞給函數的值的數組。

ring起來很複雜,那麼咱們來看看使用Apply和Call有多麼的簡單。爲了修復前面例子的問題,我將在下面你的例子中使用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();
                        });
                    });
                });
            });
        });
    });

  

你應該不想在你的代碼中遇到這樣的問題,當你當你遇到了-你將會是否是的遇到這種狀況-這裏有關於這個問題的兩種解決方案。

  1. 給你的函數命名並傳遞它們的名字做爲回調函數,而不是主函數的參數中定義匿名函數。
  2. 模塊化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函數:

function 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請求,等等)
  • 時間監聽器/處理器
  • setTimeout和setInterval方法
  • 通常狀況:精簡代碼

結束語

Javascript回調函數很是美妙且功能強大,它們爲你的web應用和代碼提供了諸多好處。你應該在有需求時使用它;或者爲了代碼的抽象性,可維護性以及可讀性而使用回調函數來重構你的代碼。

 

 

 

https://www.cnblogs.com/gaosheng-221/p/6045483.html

相關文章
相關標籤/搜索