一句話徹底理解JS中的回調(Callback)函數

作為JS的核心,回調函數和異步執行是緊密相關的,也是必須跨過去的一道個門檻。

那麼究竟什麼是回調函數(Callback),其實回調函數並不複雜,明白兩個重點即可:

1. 函數可以作為一個參數在傳遞到另一個函數中。

2. JS是異步編程語言。

簡單地說JS代碼的執行順序並不完全是從上至下按部就班完成的。
大多數語言都是同步編程語言,比如現在我們有3行代碼,那麼系統一定是一行一行按順序向下執行的,第一行執行完了,執行第二行,緊跟著最後執行第三行。
你可能會說這不是廢話嗎?
且慢,在JS裡則不盡然,比如有3行代碼,並不是排在最前面的代碼就是最先執行完畢的,很有可能是最後一行語句最先執行完,然後排在最前面的那行反而是最後執行完畢的,所以我們說JS是異步編程語言。

下面以node.js為例,舉一個例子保證你在3步之內搞清楚究竟什麼叫回調函數:

STEP 1:

<code>var fs = require("fs");
var c

function f(x) {
console.log(x)
}

function writeFile() {
fs.writeFile('input.txt', '我是通過fs.writeFile 寫入文件的內容', function (err) {
if (!err) {
console.log("文件寫入完畢!")
c = 1
}
});
}

c = 0
writeFile()
f(c)/<code>

以上代碼不難理解,就是設置一個全局變量c = 0,然後執行writeFile函數(也就是寫入一個文件input.txt),這個函數里面有一行c = 1,函數執行完畢之後再跳出來調用f()函數,f()函數很簡單,就是把打印c的值。

按照 “正常” 邏輯:

1. 首先c=0
2. 然後調用writeFile() 函數 (該函數體內部有一句c = 1)
3. 最後再調用f(c),打印c的值

因為調用writeFile()是在f(c)之前,所以c=1這條語句肯定是會被執行到,那麼結果應該是1,但是萬萬想不到,結果竟然是0,明明在writeFile()裡我們重新對c進行了賦值,為什麼結果還是0呢?

因為程序運行到writeFile()這一行的時候,是一個比較耗時的IO操作(寫文件),JS碰到這種操作並不會停在原地一直等待直到函數執行完畢,而是直接執行下一行代碼:f(c),而此時,writeFile() 函數內部的 c = 1 其實並沒有被執行,所以打印出來的結果還是0 !

那你肯定會說,要解決這個問題還不容易,我們把調用 f(c) 也放進writeFile函數里面不就行了唄!這樣就能保證c = 1之後再調用f(c)了吧?想得沒錯,就這麼簡單:

STEP 2:

<code>var fs = require("fs");
var c

function f(x) {
console.log(x)
}

function writeFile() {
fs.writeFile('input.txt', '我是通過fs.writeFile 寫入文件的內容', function (err) {
if (!err) {
console.log("文件寫入完畢!")
c = 1
f(c)
}
});
}

c = 0
writeFile() /<code>

代碼的邏輯不需要多說了吧,實在很簡單,就是把f(c)放進了writeFile()裡面,那麼c=1必然會被執行到,然後才執行f(c),不用多說,結果肯定是顯示為1。但是改成這樣並不完美,因為這麼做就相當於將f()"焊死"在writeFile()裡了,如果此處最終想調用的函數不是f()而是別的其他函數咋整?難不成要寫幾個不同的writeFile(),而他們之間的區別僅僅是最後調用的那個函數不同?這樣也太笨了吧,於是今天的主角:關鍵字

callback登場了。

STEP 3:

<code>var fs = require("fs");

function f(x) {
console.log(x)
}

function writeFile(callback) { //關鍵字callback,表示這個參數不是一個普通變量,而是一個函數
fs.writeFile('input.txt', '我是通過fs.writeFile 寫入文件的內容', function (err) {
if (!err) {
console.log("文件寫入完畢!")
c = 1
callback(c) // 因為我們傳進來的函數名是f(),所以此行相當於調用一次f(c)
}
});
}
var c = 0
writeFile(f) // 函數f作為一個實參傳進writeFile函數/<code>

經過改造後的代碼出現了兩次callback關鍵字,第一個callback出現在writeFile()的參數列表裡,起定義的作用,表示這個參數並不是一個普通變量,而是一個函數。也就是前面所說的重點1,即所謂的“以函數為參數”。

第二個callback出現在c = 1下面,表示此處“執行”從參數列表裡傳遞進來的那個函數。這樣一來,writeFile()函數在執行完畢之後到底調用哪個函數就變“活”了,如果我們想writeFile()函數執行完之後並不是像第二個例子那樣只能調用f(),而是還有別的函數比如說x() y() z(),那麼只需要寫成 writeFile(x), writeFile(y)... 就行了。

我相信你已經看明白上面的代碼,因為實在並不高深,那麼我們現在開始用一句話攻略做一個總結:

在大多數編程語言中,函數的形參總是由外往內向函數體傳遞參數,但在JS裡如果形參是關鍵字"callback"則完全相反,它表示函數體在完成某種操作後由內向外調用某個外部函數。

有時候,我們會看到一些函數的形參列表裡又出現一個函數定義的情況,初時一頭霧水,其實只要你瞭解了上面的內容,看這種直接在函數調用的時候嵌入一個function的寫法會很簡單,其本質上仍然是回調函數,因為沒有了函數名,所以也稱匿名函數。

如本例如果要寫成這種風格的話就是長成這樣了:

<code>var fs = require("fs");

function writeFile(callback) {
fs.writeFile('input.txt', '我是通過fs.writeFile 寫入文件的內容', function (err) {
if (!err) {
console.log("文件寫入完畢!")
c = 1
callback(c)
}
});
}
var c = 0
writeFile(function (x) {

console.log(x)
})/<code>

writeFile()函數不變,只是在調用它的時候,直接將函數體嵌在參數列表裡了,其作用跟上一個例子完全一樣。其實在本例中,fs.writeFile() 函數後面也跟了一個匿名回調函數 function (err) {},這個函數表示當文件寫入完畢後,就回調它,如果在寫入過程中出現了錯誤,則通過變量err攜帶出來。我相信有了前面的鋪墊,您應該能理解它的含義了,事實上這種寫法在JS裡是最常見的主流風格。

萬事必有利弊,儘管回調函數很好地解決了異步執行的問題,但是當回調函數不止一層的時候,JS就會出現函數作為參數層層嵌套的可怕場景,導致代碼的邏輯難以分析,這就是人們常說的“回調地獄”。因此,為了解決JS的異步執行回調地獄問題,人們又發明了一些其他解決方案,比如說Promises、Async functions等等,當然這又是一個冗長的話題了,在此暫且不表。

【補充】在JS裡,當然也並非所有操作都是異步的,比如for循環,無論這個for循環需要耗時多長,系統也一定會等它轉完之後才會執行下面的語句,這一點跟其他大部分同步語言是一致的。我所瞭解的會產生異步執行的操作大概有以下幾種:

  1. 定時器
  2. 建立網絡連接
  3. 讀取網絡流數據
  4. 向文件寫入數據
  5. Ajax提交
  6. 請求數據庫服務


分享到:


相關文章: