深入理解 JavaScript 回調函數
JavaScript回調函數是成為一名成功的 JavaScript 開發(fā)人員必須要了解的一個重要概念。但是我相信,在閱讀本文之后,你將能夠克服以前使用回調方法遇到的所有障礙。
在開始之前,首先要確保我們對函數的理解是扎實的。
快速回顧:JavaScript 函數
什么是函數?
函數是在其中有一組代碼的邏輯構件,用來執(zhí)行特定任務。實際上為了易于調試和維護,函數允許以更有組織的方式去編寫代碼。函數還允許代碼重用。
你只需定義一次函數,然后在需要時去調用它,而不必一次又一次地編寫相同的代碼。
聲明一個函數
現在,讓我們看看如何在 javascript 中聲明一個函數。
- 使用函數的構造函數: 在這種方法中,函數是在“函數”的構造函數的幫助下創(chuàng)建的。從技術上講,這種方法比使用函數表達式語法和函數聲明語句語法去聲明函數的方法效率要低。
- 使用函數表達式: 通常這種方法與變量分配相同。簡而言之,函數主體被視為一個表達式,并且該表達式被分配給一個變量。使用這種語法定義的函數可以是命名函數或匿名函數。
沒有名稱的函數被稱為匿名函數。匿名函數是自調用的,這意味著它會自動調用起自身。這種行為也稱為立即調用的函數表達式(IIFE)。
- 使用函數聲明: 這種方法是 JavaScript 中常用的老派方法。在關鍵字“function”之后,你必須指定函數的名稱。之后,如果函數接受多個參數或參數,也需要提及它們。雖然這部分是完全可選的。
在函數體中,函數必須將一個值返回給調用方。遇到 return 語句后,該函數將會停止執(zhí)行。在函數內部,參數將會充當局部變量。
同樣,在函數內部聲明的變量是該函數的局部變量。局部變量只能在該函數內訪問,因此具有相同名稱的變量可以輕松地用于不同的函數。
調用一個函數
在下列任何一種情況下,將調用之前聲明的函數:
- 發(fā)生事件時,例如,用戶單擊按鈕,或者用戶從下拉列表中選擇某些選項等等。
- 從 javascript 代碼中調用該函數時。
- 該函數可以自動調用,我們已經在匿名函數表達式中進行了討論。
() 運算符調用該函數。
什么是回調函數?
按照 MDN 的描述:回調函數是作為參數傳給另一個函數的函數,然后通過在外部函數內部調用該回調函數以完成某種操作。
讓我用人話解釋一下,回調函數是一個函數,將會在另一個函數完成執(zhí)行后立即執(zhí)行?;卣{函數是一個作為參數傳給另一個 JavaScript 函數的函數。這個回調函數會在傳給的函數內部執(zhí)行。
在 JavaScript 中函數被看作是一類對象。對于一類對象,我們的意思是指數字、函數或變量可以與語言中的其他實體相同。作為一類對象,可以將函數作為變量傳給其他函數,也可以從其他函數中返回這些函數。
可以執(zhí)行這種操作的函數被稱為高階函數?;卣{函數實際上是一種模式。 “模式”一詞表示解決軟件開發(fā)中常見問題的某種行之有效的方法。最好將回調函數作為回調模式去使用。
為什么我們需要回調
客戶端 JavaScript 在瀏覽器中運行,并且瀏覽器的主進程是單線程事件循環(huán)。如果我們嘗試在單線程事件循環(huán)中執(zhí)行長時間運行的操作,則會阻止該過程。從技術上講這是不好的,因為過程在等待操作完成時會停止處理其他事件。
例如,alert 語句被視為瀏覽器中 javascript 中的阻止代碼之一。如果運行 alert,則在關閉 alert 對話框窗口之前,你將無法在瀏覽器中進行任何交互。為了防止阻塞長時間運行的操作,我們使用了回調。
讓我們深入研究一下,以便使你準確了解在哪種情況下使用回調。
在上面的代碼片段中,首先執(zhí)行 getMessage()函數,然后執(zhí)行 displayMessage() 。兩者都在瀏覽器的控制臺窗口中顯示了一條消息,并且都立即執(zhí)行。
在某些情況下,一些代碼不會立即執(zhí)行。例如,如果我們假設 getMessage() 函數執(zhí)行 API 調用,則必須將請求發(fā)送到服務器并等待響應。這時我們應該如何處理呢?
如何使用回調函數
我認為與其告訴你 JavaScript 回調函數的語法,不如在前面的例子中實現回調函數更好。修改后的代碼段顯示在下面的截圖中。
為了使用回調函數,我們需要執(zhí)行某種無法立即顯示結果的任務。為了模擬這種行為,我們用 JavaScript 的 setTimeout()函數。該函數會暫停兩秒鐘,然后在控制臺窗口中顯示消息“ Hi,there”。
“顯示的消息”將被顯示在瀏覽器的控制臺窗口中。在這種情況下,首先,我們需要等待 getMessage() 函數。成功執(zhí)行此函數后,再執(zhí)行 displayMessage() 函數。
回調的工作方式
讓我解釋一下前面的例子在幕后發(fā)生的事。
從上一個例子可以看到,在 getMessage() 函數中,我們傳遞了兩個參數。第一個參數是 msg 變量,該變量顯示在瀏覽器的控制臺窗口中,第二個參數是回調函數。
現在,你可能想知道為什么將回調函數作為參數進行傳遞 —— 要實現回調函數,我們必須將一個函數作為參數傳給另一個函數。
在 getMessage() 完成任務后,我們將調用回調函數。之后,當調用 getMessage() 函數時,將引用傳給displayMessage() 函數,該函數就是回調函數。
注意,當調用 getMessage() 函數時,我們僅將其引用傳給 displayMessage() 函數。這就是為什么你不會在它旁邊看到函數調用運算符,也就是() 符號。
Javascript 回調是異步的嗎?
JavaScript 被認為是單線程腳本語言。單線程是指 JavaScript 一次執(zhí)行一個代碼塊。當 JavaScript 忙于執(zhí)行一個塊時,它不可能移到下一個塊。
換句話說,我們可以認為 JavaScript 代碼本質上總是阻塞的。但是這種阻塞性使我們無法在某些情況下編寫代碼,因為在這些情況下我們沒有辦法在執(zhí)行某些特定任務后立即得到結果。
我談論的任務包括以下情況:
- 通過對某些端點進行 API 調用來獲取數據。
- 通過發(fā)送網絡請求從遠程服務器獲取一些資源(例如,文本文件、圖像文件、二進制文件等)。
為了處理這些情況,必須編寫異步代碼,而回調函數是處理這些情況的一種方法。所以從本質上上說,回調函數是異步的。
Javascript 回調地獄
當多個異步函數一個接一個地執(zhí)行時,會產生回調地獄。它也被稱為厄運金字塔。
假設你要獲取所有 Github 用戶的列表。然后在用戶中搜索 JavaScript 庫的主要貢獻者。再然后,你想要在用戶中獲取姓名為 John 的人員的詳細信息。
為了在回調的幫助下實現這個功能,代碼應該如下所示:
- 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);
- });
- });
- });
從上面的代碼片段中,你可以看到代碼變得更加難以理解,以及難以維護和修改。這是由回調函數的嵌套而引發(fā)的。
如何避免回調地獄?
可以使用多種技術來避免回調地獄,如下所示。
- 使用promise
- 借助 async-await
- 使用 async.js 庫
使用 Async.js 庫
讓我們談談怎樣用 async.js 庫避免回調地獄。
根據 async.js 官方網站的描述:Async 是一個工具模塊,它提供了直接、強大的函數來使用異步 JavaScript。
Async.js 總共提供約 70 個函數?,F在,我們將僅討論其中兩個,即 async.waterfall() 和 async.series()。
async.waterfall()
當你要一個接一個地運行某些任務,然后將結果從上一個任務傳到下一個任務時,這個函數非常有用。它需要一個函數“任務”數組和一個最終的“回調”函數,它會在“任務”數組中所有的函數完成后,或者用錯誤對象調用“回調”之后被調用。
- 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()
當你要運行一個函數然后在所有函數成功執(zhí)行后需要獲取結果時,它很有用。 async.waterfall() 和 async.series() 之間的主要區(qū)別在于, async.series() 不會將數據從一個函數傳遞到另一個函數。
- 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 回調與閉包
閉包
用技術術語來說,閉包是捆綁在一起的函數的組合,引用了其周圍的狀態(tài)。
簡而言之,閉包允許從內部函數訪問外部函數的作用域。
要使用閉包,我們需要在一個函數內部定義另一個函數。然后,我們需要將其返回或傳給另一個函數。
回調
從概念上講,回調類似于閉包?;卣{基本上是把一個函數作為另一個函數的用法。
最后的話
希望本文能消除你對 javascript 回調函數的所有疑問。如果你覺得這篇文章有幫助,請與他人分享。