零、前言
總聽到這麼一個詞語:回調函數。
對於它的瞭解,只知道在微信的網頁授權用到了回調,以及在Angular中可以用觀察者模式進行.subscribe訂閲,但對於它原理的理解,卻是一團漿糊。直到昨天開會時,突然被問到回調函數的知識,我才意識到自己真的不理解。
一、基礎知識:JavaScript標準寫法
我們先從最簡單的寫法入手,一步一步走向回調函數。
(如果熟悉語法,請跳到第二節)
如何測試JS代碼
最簡單的方法就是,在Chrome控制枱直接輸入。
下文的Demo都在瀏覽器中演示。
JS定義函數
(説明:這裏的"函數"相當於面向對象的"方法")
/* 定義名為test的函數
傳入的參數是a
功能:輸出傳入的變量 */
var test = function(a){
console.log(a);
};
//調用方法,輸出HelloWorld
test('helloworld');
成功輸出了結果。
函數調用函數
很簡單:
/* 定義函數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();
二、數據和算法
我們先看一個寫死的函數:
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'這個字符串。
圖片顯示,這兩種方式的結果一樣。
普通函數,參數是數據,在調用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);} );
瞭解參數的對應關係
我們剛才已經知道,用傳入的方法操作固定字符串,這個字符串就是傳進來的函數的參數。
如果要用一個函數操作兩個字符串呢?
——把傳入的剪頭函數定義兩個參數。
上圖中,傳進去的剪頭函數需要兩個變量,那麼回調的時候就得傳進去兩個變量,對應關係如上圖。
如果要對同一個字符串執行兩種不同的操作呢?
——傳入兩個箭頭函數
上圖中,傳進去兩個函數,對同一字符串操作,就實現了用兩種不同方式操作同一字符串。
把上面兩種結合一下:
//
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)
}
);
請結合之前的知識,自行理解上述代碼。
四、回調函數嵌套
回調嵌套在實際生產中使用的很少,但這並不妨礙它作為我們深刻理解回調函數的一種方式。
比較下面三個函數:
//普通函數
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')});
剛才説了,傳進去的函數abc需要接收函數的能力,而再看接收的函數,正是{console.log()},也就是具備輸出的功能,所以只需要在接收到函數aFunction之後,用這個aFunction函數處理'HelloWorld'字符串就可以了。
成功輸出了結果:
五、圖解具體步驟
怕上面沒説清楚,最後用多圖流,再説一下回調嵌套的步驟。
這是原始代碼,定義了一個test,要求通過調用test來輸出HelloWorld:
第一步,調用test(),傳入函數,
傳入之後,
abc = function(aFunction) {aFunction('HelloWorld')}
第二步,用傳進來的abc處理另一個函數,需要把另一個函數作為參數aFunction,傳到abc中,此時:
aFunction = Function(def){console.log(def);}
第三步,abc接收到aFunction後,用aFunction來操作'HelloWorld'字符串,把字符串傳到aFunction中,此時:
def = 'HelloWorld';
第四步,執行console.log(def),輸出'HelloWorld'
到此為止,如果弄懂了回調函數嵌套,我們對回調函數的理解就差不多了。
六、生產環境中的觀察者
在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函數:
七、總結
我們遇到什麼回調函數,也不要怕,微笑着面對他,消除恐懼的最好辦法,就是明白回調函數是在已經有數據的前提下,傳入一個方法,然後用我們傳入的方法去操作那個已經存在的數據,堅持就是勝利,加油,奧利給!!!!!!!