深入理解 JavaScript 回調(diào)函數(shù)
JavaScript回調(diào)函數(shù)是成為一名成功的 JavaScript 開發(fā)人員必須要了解的一個(gè)重要概念。但是我相信,在閱讀本文之后,你將能夠克服以前使用回調(diào)方法遇到的所有障礙。
在開始之前,首先要確保我們對函數(shù)的理解是扎實(shí)的。
快速回顧:JavaScript 函數(shù)
什么是函數(shù)?
函數(shù)是在其中有一組代碼的邏輯構(gòu)件,用來執(zhí)行特定任務(wù)。實(shí)際上為了易于調(diào)試和維護(hù),函數(shù)允許以更有組織的方式去編寫代碼。函數(shù)還允許代碼重用。
你只需定義一次函數(shù),然后在需要時(shí)去調(diào)用它,而不必一次又一次地編寫相同的代碼。
聲明一個(gè)函數(shù)
現(xiàn)在,讓我們看看如何在 javascript 中聲明一個(gè)函數(shù)。
- 使用函數(shù)的構(gòu)造函數(shù): 在這種方法中,函數(shù)是在“函數(shù)”的構(gòu)造函數(shù)的幫助下創(chuàng)建的。從技術(shù)上講,這種方法比使用函數(shù)表達(dá)式語法和函數(shù)聲明語句語法去聲明函數(shù)的方法效率要低。
 - 使用函數(shù)表達(dá)式: 通常這種方法與變量分配相同。簡而言之,函數(shù)主體被視為一個(gè)表達(dá)式,并且該表達(dá)式被分配給一個(gè)變量。使用這種語法定義的函數(shù)可以是命名函數(shù)或匿名函數(shù)。
 
沒有名稱的函數(shù)被稱為匿名函數(shù)。匿名函數(shù)是自調(diào)用的,這意味著它會自動(dòng)調(diào)用起自身。這種行為也稱為立即調(diào)用的函數(shù)表達(dá)式(IIFE)。
- 使用函數(shù)聲明: 這種方法是 JavaScript 中常用的老派方法。在關(guān)鍵字“function”之后,你必須指定函數(shù)的名稱。之后,如果函數(shù)接受多個(gè)參數(shù)或參數(shù),也需要提及它們。雖然這部分是完全可選的。
 
在函數(shù)體中,函數(shù)必須將一個(gè)值返回給調(diào)用方。遇到 return 語句后,該函數(shù)將會停止執(zhí)行。在函數(shù)內(nèi)部,參數(shù)將會充當(dāng)局部變量。
同樣,在函數(shù)內(nèi)部聲明的變量是該函數(shù)的局部變量。局部變量只能在該函數(shù)內(nèi)訪問,因此具有相同名稱的變量可以輕松地用于不同的函數(shù)。
調(diào)用一個(gè)函數(shù)
在下列任何一種情況下,將調(diào)用之前聲明的函數(shù):
- 發(fā)生事件時(shí),例如,用戶單擊按鈕,或者用戶從下拉列表中選擇某些選項(xiàng)等等。
 - 從 javascript 代碼中調(diào)用該函數(shù)時(shí)。
 - 該函數(shù)可以自動(dòng)調(diào)用,我們已經(jīng)在匿名函數(shù)表達(dá)式中進(jìn)行了討論。
 
() 運(yùn)算符調(diào)用該函數(shù)。
什么是回調(diào)函數(shù)?
按照 MDN 的描述:回調(diào)函數(shù)是作為參數(shù)傳給另一個(gè)函數(shù)的函數(shù),然后通過在外部函數(shù)內(nèi)部調(diào)用該回調(diào)函數(shù)以完成某種操作。
讓我用人話解釋一下,回調(diào)函數(shù)是一個(gè)函數(shù),將會在另一個(gè)函數(shù)完成執(zhí)行后立即執(zhí)行。回調(diào)函數(shù)是一個(gè)作為參數(shù)傳給另一個(gè) JavaScript 函數(shù)的函數(shù)。這個(gè)回調(diào)函數(shù)會在傳給的函數(shù)內(nèi)部執(zhí)行。
在 JavaScript 中函數(shù)被看作是一類對象。對于一類對象,我們的意思是指數(shù)字、函數(shù)或變量可以與語言中的其他實(shí)體相同。作為一類對象,可以將函數(shù)作為變量傳給其他函數(shù),也可以從其他函數(shù)中返回這些函數(shù)。
可以執(zhí)行這種操作的函數(shù)被稱為高階函數(shù)。回調(diào)函數(shù)實(shí)際上是一種模式。 “模式”一詞表示解決軟件開發(fā)中常見問題的某種行之有效的方法。最好將回調(diào)函數(shù)作為回調(diào)模式去使用。
為什么我們需要回調(diào)
客戶端 JavaScript 在瀏覽器中運(yùn)行,并且瀏覽器的主進(jìn)程是單線程事件循環(huán)。如果我們嘗試在單線程事件循環(huán)中執(zhí)行長時(shí)間運(yùn)行的操作,則會阻止該過程。從技術(shù)上講這是不好的,因?yàn)檫^程在等待操作完成時(shí)會停止處理其他事件。
例如,alert 語句被視為瀏覽器中 javascript 中的阻止代碼之一。如果運(yùn)行 alert,則在關(guān)閉 alert 對話框窗口之前,你將無法在瀏覽器中進(jìn)行任何交互。為了防止阻塞長時(shí)間運(yùn)行的操作,我們使用了回調(diào)。
讓我們深入研究一下,以便使你準(zhǔn)確了解在哪種情況下使用回調(diào)。
在上面的代碼片段中,首先執(zhí)行 getMessage()函數(shù),然后執(zhí)行 displayMessage() 。兩者都在瀏覽器的控制臺窗口中顯示了一條消息,并且都立即執(zhí)行。
在某些情況下,一些代碼不會立即執(zhí)行。例如,如果我們假設(shè) getMessage() 函數(shù)執(zhí)行 API 調(diào)用,則必須將請求發(fā)送到服務(wù)器并等待響應(yīng)。這時(shí)我們應(yīng)該如何處理呢?
如何使用回調(diào)函數(shù)
我認(rèn)為與其告訴你 JavaScript 回調(diào)函數(shù)的語法,不如在前面的例子中實(shí)現(xiàn)回調(diào)函數(shù)更好。修改后的代碼段顯示在下面的截圖中。
為了使用回調(diào)函數(shù),我們需要執(zhí)行某種無法立即顯示結(jié)果的任務(wù)。為了模擬這種行為,我們用 JavaScript 的 setTimeout()函數(shù)。該函數(shù)會暫停兩秒鐘,然后在控制臺窗口中顯示消息“ Hi,there”。
“顯示的消息”將被顯示在瀏覽器的控制臺窗口中。在這種情況下,首先,我們需要等待 getMessage() 函數(shù)。成功執(zhí)行此函數(shù)后,再執(zhí)行 displayMessage() 函數(shù)。
回調(diào)的工作方式
讓我解釋一下前面的例子在幕后發(fā)生的事。
從上一個(gè)例子可以看到,在 getMessage() 函數(shù)中,我們傳遞了兩個(gè)參數(shù)。第一個(gè)參數(shù)是 msg 變量,該變量顯示在瀏覽器的控制臺窗口中,第二個(gè)參數(shù)是回調(diào)函數(shù)。
現(xiàn)在,你可能想知道為什么將回調(diào)函數(shù)作為參數(shù)進(jìn)行傳遞 —— 要實(shí)現(xiàn)回調(diào)函數(shù),我們必須將一個(gè)函數(shù)作為參數(shù)傳給另一個(gè)函數(shù)。
在 getMessage() 完成任務(wù)后,我們將調(diào)用回調(diào)函數(shù)。之后,當(dāng)調(diào)用 getMessage() 函數(shù)時(shí),將引用傳給displayMessage() 函數(shù),該函數(shù)就是回調(diào)函數(shù)。
注意,當(dāng)調(diào)用 getMessage() 函數(shù)時(shí),我們僅將其引用傳給 displayMessage() 函數(shù)。這就是為什么你不會在它旁邊看到函數(shù)調(diào)用運(yùn)算符,也就是() 符號。
Javascript 回調(diào)是異步的嗎?
JavaScript 被認(rèn)為是單線程腳本語言。單線程是指 JavaScript 一次執(zhí)行一個(gè)代碼塊。當(dāng) JavaScript 忙于執(zhí)行一個(gè)塊時(shí),它不可能移到下一個(gè)塊。
換句話說,我們可以認(rèn)為 JavaScript 代碼本質(zhì)上總是阻塞的。但是這種阻塞性使我們無法在某些情況下編寫代碼,因?yàn)樵谶@些情況下我們沒有辦法在執(zhí)行某些特定任務(wù)后立即得到結(jié)果。
我談?wù)摰娜蝿?wù)包括以下情況:
- 通過對某些端點(diǎn)進(jìn)行 API 調(diào)用來獲取數(shù)據(jù)。
 - 通過發(fā)送網(wǎng)絡(luò)請求從遠(yuǎn)程服務(wù)器獲取一些資源(例如,文本文件、圖像文件、二進(jìn)制文件等)。
 
為了處理這些情況,必須編寫異步代碼,而回調(diào)函數(shù)是處理這些情況的一種方法。所以從本質(zhì)上上說,回調(diào)函數(shù)是異步的。
Javascript 回調(diào)地獄
當(dāng)多個(gè)異步函數(shù)一個(gè)接一個(gè)地執(zhí)行時(shí),會產(chǎn)生回調(diào)地獄。它也被稱為厄運(yùn)金字塔。
假設(shè)你要獲取所有 Github 用戶的列表。然后在用戶中搜索 JavaScript 庫的主要貢獻(xiàn)者。再然后,你想要在用戶中獲取姓名為 John 的人員的詳細(xì)信息。
為了在回調(diào)的幫助下實(shí)現(xiàn)這個(gè)功能,代碼應(yīng)該如下所示:
- http.get('https://api.github.com/users', function(users) {
 - /* Display all users */
 - console.log(users);
 - http.get('https://api.github.com/repos/javascript/contributors?q=contributions&order=desc', function(contributors) {
 - /* Display all top contributors */
 - console.log(contributors);
 - http.get('https://api.github.com/users/Jhon', function(userData) {
 - /* Display user with username 'Jhon' */
 - console.log(userData);
 - });
 - });
 - });
 
從上面的代碼片段中,你可以看到代碼變得更加難以理解,以及難以維護(hù)和修改。這是由回調(diào)函數(shù)的嵌套而引發(fā)的。
如何避免回調(diào)地獄?
可以使用多種技術(shù)來避免回調(diào)地獄,如下所示。
- 使用promise
 - 借助 async-await
 - 使用 async.js 庫
 
使用 Async.js 庫
讓我們談?wù)勗鯓佑?async.js 庫避免回調(diào)地獄。
根據(jù) async.js 官方網(wǎng)站的描述:Async 是一個(gè)工具模塊,它提供了直接、強(qiáng)大的函數(shù)來使用異步 JavaScript。
Async.js 總共提供約 70 個(gè)函數(shù)?,F(xiàn)在,我們將僅討論其中兩個(gè),即 async.waterfall() 和 async.series()。
async.waterfall()
當(dāng)你要一個(gè)接一個(gè)地運(yùn)行某些任務(wù),然后將結(jié)果從上一個(gè)任務(wù)傳到下一個(gè)任務(wù)時(shí),這個(gè)函數(shù)非常有用。它需要一個(gè)函數(shù)“任務(wù)”數(shù)組和一個(gè)最終的“回調(diào)”函數(shù),它會在“任務(wù)”數(shù)組中所有的函數(shù)完成后,或者用錯(cuò)誤對象調(diào)用“回調(diào)”之后被調(diào)用。
- var async = require('async');
 - async.waterfall([
 - function(callback) {
 - /*
 - Here, the first argument value is null, it indicates that
 - the next function will be executed from the array of functions.
 - If the value was true or any string then final callback function
 - will be executed, other remaining functions in the array
 - will not be executed.
 - */
 - callback(null, 'one', 'two');
 - },
 - function(param1, param2, callback) {
 - // param1 now equals 'one' and param2 now equals 'two'
 - callback(null, 'three');
 - },
 - function(param1, callback) {
 - // param1 now equals 'three'
 - callback(null, 'done');
 - }
 - ], function (err, result) {
 - /*
 - This is the final callback function.
 - result now equals 'done'
 - */
 - });
 
async.series()
當(dāng)你要運(yùn)行一個(gè)函數(shù)然后在所有函數(shù)成功執(zhí)行后需要獲取結(jié)果時(shí),它很有用。 async.waterfall() 和 async.series() 之間的主要區(qū)別在于, async.series() 不會將數(shù)據(jù)從一個(gè)函數(shù)傳遞到另一個(gè)函數(shù)。
- async.series([
 - function(callback) {
 - // do some stuff ...
 - callback(null, 'one');
 - },
 - function(callback) {
 - // do some more stuff ...
 - callback(null, 'two');
 - }
 - ],
 - // optional callback
 - function(err, results) {
 - // results is now equal to ['one', 'two']
 - });
 
Javascript 回調(diào)與閉包
閉包
用技術(shù)術(shù)語來說,閉包是捆綁在一起的函數(shù)的組合,引用了其周圍的狀態(tài)。
簡而言之,閉包允許從內(nèi)部函數(shù)訪問外部函數(shù)的作用域。
要使用閉包,我們需要在一個(gè)函數(shù)內(nèi)部定義另一個(gè)函數(shù)。然后,我們需要將其返回或傳給另一個(gè)函數(shù)。
回調(diào)
從概念上講,回調(diào)類似于閉包?;卣{(diào)基本上是把一個(gè)函數(shù)作為另一個(gè)函數(shù)的用法。
最后的話
希望本文能消除你對 javascript 回調(diào)函數(shù)的所有疑問。如果你覺得這篇文章有幫助,請與他人分享。

















 
 
 





 
 
 
 