博客 / 詳情

返回

從零起步,真正理解Javascript回調函數

零、前言

總聽到這麼一個詞語:回調函數。
對於它的瞭解,只知道在微信網頁授權用到了回調,以及在Angular中可以用觀察者模式進行.subscribe訂閲,但對於它原理的理解,卻是一團漿糊。直到昨天開會時,突然被問到回調函數的知識,我才意識到自己真的不理解。

一、基礎知識:JavaScript標準寫法

我們先從最簡單的寫法入手,一步一步走向回調函數。
(如果熟悉語法,請跳到第二節)

如何測試JS代碼

最簡單的方法就是,在Chrome控制枱直接輸入。
圖片.png
下文的Demo都在瀏覽器中演示。

JS定義函數

(説明:這裏的"函數"相當於面向對象的"方法"

/*  定義名為test的函數
    傳入的參數是a
    功能:輸出傳入的變量  */

var test = function(a){
    console.log(a);
};

//調用方法,輸出HelloWorld

test('helloworld');

圖片.png
成功輸出了結果。

函數調用函數

很簡單:

/*  定義函數sum
    傳入兩個參數a,b
    作用:求a,b的和   */
var sum = function(a,b)
{
    return a+b;
};

/*  定義函數test
    調用sum
    傳入參數1,2 */
var test = function()
{
    result = sum(1,2);
    console.log(result);
}

//調用test,啓動程序
test();

圖片.png

二、數據和算法

我們先看一個寫死的函數:

var test = function( ){
    console.log("HelloWorld");
};

這個函數事實上是沒有意義的,因為它沒有輸入,不會變化,無論重複運行多少次,它的結果都是一樣的。
一個函數裏面,既有算法又有數據:"算法"指的是輸出字符串的這個操作,"數據"這的是輸出的內容'HelloWorld'。所以如果我們想讓這個函數發揮作用,就要讓它可以變化

(備用)我們可以藉助最原始時代的編程思想來理解:在早期的計算機思想中,數據算法是分離的,算法被寫成一段段代碼,數據是用來被算法操作的。

程序等於數據加算法。藉助這種想法,我們認為,在一個函數中,只要數據算法其中一個是可以變化的,那麼函數就是有意義的。

改變數據——普通的函數

我們先想到的肯定是改變數據,把一個寫死的函數加上參數,它就變“活了”。

//寫死的函數
var test = function( ){
    console.log("HelloWorld");
};
test();

//加上參數
var test = function(string){
    console.log(string);
};
test('HelloWorld');

這樣,把一段永遠不會變化的代碼,變成了可以在調用時根據不同輸入來獲得不同輸出的程序。

改變算法——回調函數

初次揭開回調函數的面紗:
把一個寫死的函數變活,可以傳入數據,用相同的算法對不同的數據進行處理;當然也可以傳入一個算法,用不同的算法對相同的數據進行處理,而後者,正是回調函數。

用一句話概括:在直接調用函數A()時,把另一個函數B()作為參數,傳入函數A()裏面,以此來通過函數A()間接調用函數B()。

比較下面兩個函數的異同:

//函數1
var test = function(abc){
    console.log(abc);
};
//函數1的調用
test('HelloWorld');

//函數2
var test = function(abc){
    abc('Helloworld');
};
//函數2的調用
test( function(words) {console.log(words);}  );

這兩個函數有着同樣的參數,都是abc,只不過,函數1是普通函數,參數abc是作為數據傳入的,然後用函數1的語句來操作abc;
而函數2是回調函數,參數是作為算法傳入的,然後用傳進來的這個函數abc來操作'HelloWorld'這個字符串。
圖片.png
圖片顯示,這兩種方式的結果一樣。
圖片.png

普通函數,參數是數據,在調用test()時,傳入HelloWorld字符串,那麼abc就是這個字符串,test函數對傳入的字符串執行了輸出操作;
而回調函數,參數是函數,在調用test()時,傳入輸出字符串的方法,那麼abc就是輸出字符串的方法,test函數將會用傳進來的輸出字符串的方法對一個固定字符串HelloWorld執行操作。與此同時,這個HelloWorld字符串,又成了傳到test函數的這個function(words) {console.log(words);}函數的參數,如果傳入的參數有名字的話,在調用test時,在函數內部,等價於發生瞭如下操作:

{
    //調用test傳進來一個函數之後,abc就是那個函數
    abc = function(words) {console.log(words);};
    //用abc操作字符串,'HelloWorld'變成了傳進來的函數的參數
    abc('HelloWorld');
}

三,深入回調函數

我們已經理解了初級階段的回調,但目前,傳入的函數還處於function(){}的形式,這種寫法是原始寫法,相當於定義了一個新函數然後傳進去,不僅脱離生產環境,而且有很多侷限(比如this.作用域問題),下一步,要把這種形式改為剪頭函數。

一個函數只用一次,並且不需要直到它的名字時,可以用匿名函數來簡化。
在解決this.作用域時,又將匿名函數轉化為箭頭函數

對於匿名函數來説,this就是指的這個匿名函數,由於這個函數不是某個對象的方法,不屬於任何一個類,所以這個匿名函數中使用this,自然也就無法找到想要的那個對象。
而箭頭函數的特殊之處在於,它的this,指的是調用它的對象。誰調用這個箭頭函數,this就指向誰。所以可以用箭頭函數輕鬆的獲取到調用它的對象的屬性。

//以下兩種寫法等價

function (words) {console.log(words);}

(words) => {console.log(words);}

可以看出,箭頭函數省略了function標識,只留下了參數words和函數體console.log(),二者用箭頭連接。
這種省略寫法更貼近生產環境:

//函數2
var test = function(abc){
    abc('Helloworld');
};
//函數2的調用
test( (words) => {console.log(words);}  );

瞭解參數的對應關係

圖片.png
我們剛才已經知道,用傳入的方法操作固定字符串,這個字符串就是傳進來的函數的參數。

如果要用一個函數操作兩個字符串呢?
——把傳入的剪頭函數定義兩個參數。

圖片.png
圖片.png
上圖中,傳進去的剪頭函數需要兩個變量,那麼回調的時候就得傳進去兩個變量,對應關係如上圖。

如果要對同一個字符串執行兩種不同的操作呢?
——傳入兩個箭頭函數

圖片.png
圖片.png

上圖中,傳進去兩個函數,對同一字符串操作,就實現了用兩種不同方式操作同一字符串。

把上面兩種結合一下:

//
var test = function(abc, def){
    abc('HelloWorld1', 'HelloWorld2');
    def('HelloWorld1', 'HelloWorld2');
};
//
test((words1,words2) => {
        console.log('我是箭頭1,我輸出'+words1);
        console.log('我是箭頭1,我輸出'+words2);
    },
    (words1,words2)=> {
        console.log('我是箭頭2,我輸出'+words1);
        console.log('我是箭頭2,我輸出'+words2)
    }
    );

圖片.png
請結合之前的知識,自行理解上述代碼。

四、回調函數嵌套

回調嵌套在實際生產中使用的很少,但這並不妨礙它作為我們深刻理解回調函數的一種方式。
比較下面三個函數:

//普通函數
var test = function(abc){
    console.log(abc);
}

//回調函數
var test = function(abc){
    abc('HelloWorld');
};

//回調函數嵌套
var test = function(abc){
    abc( (def) => {console.log(def);} );
}

練習題:問,以上三種情況下,如果分別調用三個函數,輸出HelloWorld字符串?

第一種早就學會了,直接調用就可以:

//普通函數
test('HelloWorld');

第二種也已經會了,有了HelloWorld的數據,我們需要傳進去的是操作這個數據的方法,所以:

//回調函數
test( (words) => {console.log(words);});

主要説的是第三種,
回調函數是傳入一個函數,用傳入的函數abc去操作一個數據'HelloWorld'。這個被操作的數據,作為傳進來的函數abc的參數。
而再看回調函數嵌套,它也是傳進去一個函數abc,但不同的是,它是用這個傳進來的函數去操作另一個函數。
此時,我們傳入的abc函數需要一種可以接收函數的能力,而不再是接收變量的能力。
所以怎麼辦?——在傳進去的這個函數abc中再使用一次回調,使得abc接收的參數是一個函數,而不是一個變量:

//回調函數嵌套
test( (aFunction) => {aFunction('HelloWorld')} );

//如果看不明白,把箭頭函數復原,如下
test( function(aFunction) {aFunction('HelloWorld')});

圖片.png
剛才説了,傳進去的函數abc需要接收函數的能力,而再看接收的函數,正是{console.log()},也就是具備輸出的功能,所以只需要在接收到函數aFunction之後,用這個aFunction函數處理'HelloWorld'字符串就可以了。
成功輸出了結果:
圖片.png

五、圖解具體步驟

怕上面沒説清楚,最後用多圖流,再説一下回調嵌套的步驟。

這是原始代碼,定義了一個test,要求通過調用test來輸出HelloWorld:
圖片.png

第一步,調用test(),傳入函數,
傳入之後,

圖片.png

abc = function(aFunction) {aFunction('HelloWorld')}

第二步,用傳進來的abc處理另一個函數,需要把另一個函數作為參數aFunction,傳到abc中,此時:

圖片.png

aFunction = Function(def){console.log(def);}

第三步,abc接收到aFunction後,用aFunction來操作'HelloWorld'字符串,把字符串傳到aFunction中,此時:

圖片.png

def = 'HelloWorld';

第四步,執行console.log(def),輸出'HelloWorld'
圖片.png

到此為止,如果弄懂了回調函數嵌套,我們對回調函數的理解就差不多了。

六、生產環境中的觀察者

在Angular中,有一個.subcribe訂閲,這個就是回調,以前這知道這麼用,但現在我們可以解釋一下它的原理了!

先上代碼:

//向8080端口的helloWorld路徑發起請求
httpClient.get('http://localhost:8080/helloWorld')
  .subscribe(
    function success(data) {
      console.log('請求成功');
      console.log(data);
    },
    function error(data) {
      console.log('請求失敗');
      console.log(data);
    });

httpClient的作用是發起HTTP請求,並且記錄請求狀態。
.get設定請求地址。
.subscript是設定請求完成的操作。

我們的目的是,輸出請求之後的信息,以便讓我們知道請求是否成功。並且我們已經知道httpClient會記錄請求的狀態,這個“狀態”就是個變量。
既然變量就在那裏放着不動,我們只需要想辦法,去操作這個狀態的變量就可以了,——傳入兩個函數,success和error,在請求成功之後調用success,如果失敗調用error。再次強調,它已經有數據了,我們傳進去的是函數,請求完畢後,就會用我們傳入的函數去操作這個請求狀態。我們傳入的“輸出”,他就“輸出”這個狀態!

//向8080端口的helloWorld路徑發起請求
httpClient.get('http://localhost:8080/helloWorld')
  .subscribe(
    function success(data) {
      console.log('請求成功');
      console.log(data);
    },
    function error(data) {
      console.log('請求失敗');
      console.log(data);
    });

//系統中的subscirbe函數
function subscribe(success,error)
{
    request_success;//請求是否成功
    request_data;//請求數據

    if (request_success == 1) {
        success(request_data);
    }
    else{
        error(request_data);
    }
}

為了便於理解,我寫了一個假的subscribe函數:
圖片.png

七、總結

我們遇到什麼回調函數,也不要怕,微笑着面對他,消除恐懼的最好辦法,就是明白回調函數是在已經有數據的前提下,傳入一個方法,然後用我們傳入的方法去操作那個已經存在的數據,堅持就是勝利,加油,奧利給!!!!!!!

user avatar yaofly 頭像 dujing_5b7edb9db0b1c 頭像 hightopo 頭像 columsys 頭像 weirdo_5f6c401c6cc86 頭像 frontoldman 頭像 niumingxin 頭像 fyuanlove 頭像 yangkaiqiang 頭像 nealyang231 頭像 air_clou_d 頭像 kandole 頭像
31 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.