本文將深入探討現代JavaScript開發中三個至關重要的概念:閉包、回調函數和立即執行函數表達式 (IIFE)。我們已詳細了解變量作用域和提升,現在讓我們完成探索之旅。
核心要點
閉包
在JavaScript中,閉包是任何保留對其父作用域變量引用的函數,即使父函數已返回。
實際上,任何函數都可以被認為是閉包,因為正如我們在本教程第一部分的變量作用域部分中學到的那樣,函數可以引用或訪問:
因此,您可能已經在不知不覺中使用了閉包。但我們的目標不僅僅是使用它們——而是理解它們。如果我們不了解它們的工作原理,我們就無法正確地使用它們。為此,我們將上述閉包定義分解為三個易於理解的要點。
要點1:您可以引用在當前函數外部定義的變量。
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France
在這個代碼示例中,printLocation()
函數引用了封閉(父)setLocation()
函數的 country
變量和 city
參數。結果是,當調用 setLocation()
時,printLocation()
成功地使用前者的變量和參數輸出“You are in Paris, France”。
要點2:內部函數即使在外部函數返回後,也可以引用外部函數中定義的變量。
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France
這與第一個示例幾乎相同,只是這次 printLocation()
在外部 setLocation()
函數中返回,而不是立即調用。因此,currentLocation
的值是內部 printLocation()
函數。
如果我們像這樣提醒 currentLocation
– alert(currentLocation);
– 我們將得到以下輸出:
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } return printLocation; } var currentLocation = setLocation("Paris"); currentLocation(); // 输出:You are in Paris, France
正如我們所看到的,printLocation()
在其詞法作用域之外執行。 setLocation()
似乎消失了,但 printLocation()
仍然可以訪問並“記住”其變量(country
)和參數(city
)。
閉包(內部函數)能夠記住其周圍的作用域(外部函數),即使它在其詞法作用域之外執行。因此,您可以稍後在程序中的任何時間調用它。
要點3:內部函數通過引用存儲其外部函數的變量,而不是通過值。
function printLocation () { console.log("You are in " + city + ", " + country); }
這裡 cityLocation()
返回一個包含兩個閉包的對象——get()
和 set()
——它們都引用外部變量 city
。 get()
獲取 city
的當前值,而 set()
更新它。當第二次調用 myLocation.get()
時,它輸出 city
的更新(當前)值——“Sydney”——而不是默認的“Paris”。
因此,閉包既可以讀取也可以更新其存儲的變量,並且這些更新對任何訪問它們的閉包都是可見的。這意味著閉包存儲的是對其外部變量的引用,而不是複制其值。這是一個非常重要的點,因為不知道這一點可能會導致一些難以發現的邏輯錯誤——正如我們在“立即執行函數表達式 (IIFE)”部分將看到的。
閉包的一個有趣特性是,閉包中的變量會自動隱藏。閉包在其封閉變量中存儲數據,而不提供直接訪問它們的方法。改變這些變量的唯一方法是間接地訪問它們。例如,在最後一個代碼片段中,我們看到我們只能通過使用 get()
和 set()
閉包來間接修改變量 city
。
我們可以利用這種行為在對像中存儲私有數據。與其將數據存儲為對象的屬性,不如將其存儲為構造函數中的變量,然後使用閉包作為引用這些變量的方法。
如您所見,閉包周圍沒有什麼神秘或深奧的東西——只需要記住三個簡單的要點。
回調函數
在JavaScript中,函數是一等公民。這一事實的結果之一是,函數可以作為參數傳遞給其他函數,也可以由其他函數返回。
將其他函數作為參數或返回函數作為其結果的函數稱為高階函數,作為參數傳遞的函數稱為回調函數。它被稱為“回調”,因為在某個時間點,它會被高階函數“回調”。
回調函數有很多日常用途。其中之一是當我們使用瀏覽器窗口對象的 setTimeout()
和 setInterval()
方法時——這些方法接受並執行回調函數:
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France
另一個例子是當我們將事件監聽器附加到頁面上的元素時。通過這樣做,我們實際上提供了一個指向回調函數的指針,當事件發生時將調用該函數。
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } return printLocation; } var currentLocation = setLocation("Paris"); currentLocation(); // 输出:You are in Paris, France
理解高階函數和回調函數工作原理的最簡單方法是創建您自己的高階函數和回調函數。所以,讓我們現在創建一個:
function printLocation () { console.log("You are in " + city + ", " + country); }
這裡我們創建了一個函數 fullName()
,它接受三個參數——兩個用於名字和姓氏,一個用於回調函數。然後,在 console.log()
語句之後,我們放置一個函數調用,該調用將觸發實際的回調函數——在 fullName()
下面定義的 greeting()
函數。最後,我們調用 fullName()
,其中 greeting()
作為變量傳遞——沒有括號——因為我們不希望它立即執行,而只是希望指向它以便稍後由 fullName()
使用。
我們正在傳遞函數定義,而不是函數調用。這可以防止回調函數立即執行,這與回調函數背後的理念不符。作為函數定義傳遞,它們可以在任何時間和包含函數中的任何點執行。此外,因為回調函數的行為就像它們實際上放置在該函數內部一樣,所以它們實際上是閉包:它們可以訪問包含函數的變量和參數,甚至可以訪問全局作用域中的變量。
回調函數可以是現有函數(如前面的示例所示),也可以是匿名函數,我們在調用高階函數時創建匿名函數,如以下示例所示:
function cityLocation() { var city = "Paris"; return { get: function() { console.log(city); }, set: function(newCity) { city = newCity; } }; } var myLocation = cityLocation(); myLocation.get(); // 输出:Paris myLocation.set('Sydney'); myLocation.get(); // 输出:Sydney
回調函數在JavaScript庫中大量使用,以提供通用性和可重用性。它們允許輕鬆自定義和/或擴展庫方法。此外,代碼更易於維護,更簡潔易讀。每當您需要將不必要的重複代碼模式轉換為更抽象/通用的函數時,回調函數都會派上用場。
假設我們需要兩個函數——一個打印已發布文章信息的函數,另一個打印已發送消息信息的函數。我們創建了它們,但我們注意到我們的邏輯的一部分在這兩個函數中都重複了。我們知道,在一個地方擁有相同的一段代碼是不必要的,而且難以維護。那麼,解決方案是什麼呢?讓我們在下一個示例中說明它:
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France
我們在這裡所做的是將重複的代碼模式(console.log(item)
和var date = new Date()
)放入一個單獨的通用函數(publish()
)中,只將特定數據保留在其他函數中——這些函數現在是回調函數。這樣,使用同一個函數,我們可以打印各種相關事物的相關信息——消息、文章、書籍、雜誌等等。您唯一需要做的就是為每種類型創建一個專門的回調函數,並將其作為參數傳遞給 publish()
函數。
立即執行函數表達式 (IIFE)
立即執行函數表達式,或 IIFE(發音為“iffy”),是一個立即在其創建後執行的函數表達式(命名或匿名)。
此模式有兩種略微不同的語法變體:
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } return printLocation; } var currentLocation = setLocation("Paris"); currentLocation(); // 输出:You are in Paris, France
要將常規函數轉換為 IIFE,您需要執行兩個步驟:
還需要記住三件事:
首先,如果您將函數分配給變量,則不需要將整個函數括在括號中,因為它已經是表達式了:
function printLocation () { console.log("You are in " + city + ", " + country); }
其次,IIFE 結尾需要分號,否則您的代碼可能無法正常工作。
第三,您可以向 IIFE 傳遞參數(它畢竟是一個函數),如下面的示例所示:
function cityLocation() { var city = "Paris"; return { get: function() { console.log(city); }, set: function(newCity) { city = newCity; } }; } var myLocation = cityLocation(); myLocation.get(); // 输出:Paris myLocation.set('Sydney'); myLocation.get(); // 输出:Sydney
將全局對像作為參數傳遞給 IIFE 是一種常見模式,以便在函數內部訪問它而無需使用 window
對象,這使得代碼獨立於瀏覽器環境。以下代碼創建了一個變量 global
,無論您使用什麼平台,它都將引用全局對象:
function showMessage(message) { setTimeout(function() { alert(message); }, 3000); } showMessage('Function called 3 seconds ago');
這段代碼在瀏覽器中(全局對像是 window
)或 Node.js 環境中(我們使用特殊變量 global
引用全局對象)都能工作。
IIFE 的一大好處是,使用它時,您不必擔心用臨時變量污染全局空間。您在 IIFE 內部定義的所有變量都將是局部的。讓我們檢查一下:
<!-- HTML --> <button id="btn">Click me</button> <!-- JavaScript --> function showMessage() { alert('Woohoo!'); } var el = document.getElementById("btn"); el.addEventListener("click", showMessage);
在這個示例中,第一個 console.log()
語句工作正常,但第二個語句失敗了,因為由於 IIFE,變量 today
和 currentTime
變成了局部變量。
我們已經知道閉包會保留對外部變量的引用,因此,它們會返回最新/更新的值。那麼,您認為以下示例的輸出是什麼?
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France
您可能期望水果的名稱會以一秒鐘的間隔一個接一個地打印出來。但是,實際上,輸出是四次“undefined”。那麼,問題出在哪裡呢?
問題在於,在 console.log()
語句中,i
的值對於循環的每次迭代都等於 4。並且,由於我們在 fruits
數組中索引 4 處沒有任何內容,因此輸出為“undefined”。 (記住,在JavaScript中,數組的索引從 0 開始。)當 i
等於 4 時,循環終止。
為了解決這個問題,我們需要為循環創建的每個函數提供一個新的作用域——這將捕獲 i
變量的當前狀態。我們通過在 IIFE 中關閉 setTimeout()
方法,並定義一個私有變量來保存 i
的當前副本,來做到這一點。
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } return printLocation; } var currentLocation = setLocation("Paris"); currentLocation(); // 输出:You are in Paris, France
我們還可以使用以下變體,它執行相同的任務:
function printLocation () { console.log("You are in " + city + ", " + country); }
IIFE 通常用於創建作用域以封裝模塊。在模塊內,存在一個自包含的私有作用域,可以防止意外修改。這種技術稱為模塊模式,是使用閉包管理作用域的強大示例,它在許多現代JavaScript庫(例如jQuery和Underscore)中大量使用。
結論
本教程的目的是盡可能清晰簡潔地介紹這些基本概念——作為一組簡單的原則或規則。很好地理解它們是成為一名成功且高效的JavaScript開發人員的關鍵。
為了更詳細和深入地解釋此處介紹的主題,我建議您閱讀 Kyle Simpson 的《你不知道JS:作用域與閉包》。
(後續內容,即FAQ部分,由於篇幅過長,已省略。如有需要,請提出具體問題。)
以上是揭開JavaScript關閉,回調和IIFES的神秘面紗的詳細內容。更多資訊請關注PHP中文網其他相關文章!