本文将深入探讨现代JavaScript开发中三个至关重要的概念:闭包、回调函数和立即执行函数表达式 (IIFE)。我们已详细了解变量作用域和提升,现在让我们完成探索之旅。
核心要点
闭包
在JavaScript中,闭包是任何保留对其父作用域变量引用的函数,即使父函数已返回。
实际上,任何函数都可以被认为是闭包,因为正如我们在本教程第一部分的变量作用域部分中学到的那样,函数可以引用或访问:
因此,您可能已经在不知不觉中使用了闭包。但我们的目标不仅仅是使用它们——而是理解它们。如果我们不了解它们的工作原理,我们就无法正确地使用它们。为此,我们将上述闭包定义分解为三个易于理解的要点。
要点1:您可以引用在当前函数外部定义的变量。
<code class="language-javascript">function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France</code>
在这个代码示例中,printLocation()
函数引用了封闭(父)setLocation()
函数的 country
变量和 city
参数。结果是,当调用 setLocation()
时,printLocation()
成功地使用前者的变量和参数输出“You are in Paris, France”。
要点2:内部函数即使在外部函数返回后,也可以引用外部函数中定义的变量。
<code class="language-javascript">function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France</code>
这与第一个示例几乎相同,只是这次 printLocation()
在外部 setLocation()
函数中返回,而不是立即调用。因此,currentLocation
的值是内部 printLocation()
函数。
如果我们像这样提醒 currentLocation
– alert(currentLocation);
– 我们将得到以下输出:
<code class="language-javascript">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</code>
正如我们所看到的,printLocation()
在其词法作用域之外执行。setLocation()
似乎消失了,但 printLocation()
仍然可以访问并“记住”其变量(country
)和参数(city
)。
闭包(内部函数)能够记住其周围的作用域(外部函数),即使它在其词法作用域之外执行。因此,您可以稍后在程序中的任何时间调用它。
要点3:内部函数通过引用存储其外部函数的变量,而不是通过值。
<code class="language-javascript">function printLocation () { console.log("You are in " + city + ", " + country); }</code>
这里 cityLocation()
返回一个包含两个闭包的对象——get()
和 set()
——它们都引用外部变量 city
。get()
获取 city
的当前值,而 set()
更新它。当第二次调用 myLocation.get()
时,它输出 city
的更新(当前)值——“Sydney”——而不是默认的“Paris”。
因此,闭包既可以读取也可以更新其存储的变量,并且这些更新对任何访问它们的闭包都是可见的。这意味着闭包存储的是对其外部变量的引用,而不是复制其值。这是一个非常重要的点,因为不知道这一点可能会导致一些难以发现的逻辑错误——正如我们在“立即执行函数表达式 (IIFE)”部分将看到的。
闭包的一个有趣特性是,闭包中的变量会自动隐藏。闭包在其封闭变量中存储数据,而不提供直接访问它们的方法。改变这些变量的唯一方法是间接地访问它们。例如,在最后一个代码片段中,我们看到我们只能通过使用 get()
和 set()
闭包来间接修改变量 city
。
我们可以利用这种行为在对象中存储私有数据。与其将数据存储为对象的属性,不如将其存储为构造函数中的变量,然后使用闭包作为引用这些变量的方法。
如您所见,闭包周围没有什么神秘或深奥的东西——只需要记住三个简单的要点。
回调函数
在JavaScript中,函数是一等公民。这一事实的结果之一是,函数可以作为参数传递给其他函数,也可以由其他函数返回。
将其他函数作为参数或返回函数作为其结果的函数称为高阶函数,作为参数传递的函数称为回调函数。它被称为“回调”,因为在某个时间点,它会被高阶函数“回调”。
回调函数有很多日常用途。其中之一是当我们使用浏览器窗口对象的 setTimeout()
和 setInterval()
方法时——这些方法接受并执行回调函数:
<code class="language-javascript">function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France</code>
另一个例子是当我们将事件监听器附加到页面上的元素时。通过这样做,我们实际上提供了一个指向回调函数的指针,当事件发生时将调用该函数。
<code class="language-javascript">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</code>
理解高阶函数和回调函数工作原理的最简单方法是创建您自己的高阶函数和回调函数。所以,让我们现在创建一个:
<code class="language-javascript">function printLocation () { console.log("You are in " + city + ", " + country); }</code>
这里我们创建了一个函数 fullName()
,它接受三个参数——两个用于名字和姓氏,一个用于回调函数。然后,在 console.log()
语句之后,我们放置一个函数调用,该调用将触发实际的回调函数——在 fullName()
下面定义的 greeting()
函数。最后,我们调用 fullName()
,其中 greeting()
作为变量传递——没有括号——因为我们不希望它立即执行,而只是希望指向它以便稍后由 fullName()
使用。
我们正在传递函数定义,而不是函数调用。这可以防止回调函数立即执行,这与回调函数背后的理念不符。作为函数定义传递,它们可以在任何时间和包含函数中的任何点执行。此外,因为回调函数的行为就像它们实际上放置在该函数内部一样,所以它们实际上是闭包:它们可以访问包含函数的变量和参数,甚至可以访问全局作用域中的变量。
回调函数可以是现有函数(如前面的示例所示),也可以是匿名函数,我们在调用高阶函数时创建匿名函数,如以下示例所示:
<code class="language-javascript">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</code>
回调函数在JavaScript库中大量使用,以提供通用性和可重用性。它们允许轻松自定义和/或扩展库方法。此外,代码更易于维护,更简洁易读。每当您需要将不必要的重复代码模式转换为更抽象/通用的函数时,回调函数都会派上用场。
假设我们需要两个函数——一个打印已发布文章信息的函数,另一个打印已发送消息信息的函数。我们创建了它们,但我们注意到我们的逻辑的一部分在这两个函数中都重复了。我们知道,在一个地方拥有相同的一段代码是不必要的,而且难以维护。那么,解决方案是什么呢?让我们在下一个示例中说明它:
<code class="language-javascript">function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France</code>
我们在这里所做的是将重复的代码模式(console.log(item)
和 var date = new Date()
)放入一个单独的通用函数(publish()
)中,只将特定数据保留在其他函数中——这些函数现在是回调函数。这样,使用同一个函数,我们可以打印各种相关事物的相关信息——消息、文章、书籍、杂志等等。您唯一需要做的就是为每种类型创建一个专门的回调函数,并将其作为参数传递给 publish()
函数。
立即执行函数表达式 (IIFE)
立即执行函数表达式,或 IIFE(发音为“iffy”),是一个立即在其创建后执行的函数表达式(命名或匿名)。
此模式有两种略微不同的语法变体:
<code class="language-javascript">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</code>
要将常规函数转换为 IIFE,您需要执行两个步骤:
还需要记住三件事:
首先,如果您将函数分配给变量,则不需要将整个函数括在括号中,因为它已经是表达式了:
<code class="language-javascript">function printLocation () { console.log("You are in " + city + ", " + country); }</code>
其次,IIFE 结尾需要分号,否则您的代码可能无法正常工作。
第三,您可以向 IIFE 传递参数(它毕竟是一个函数),如下面的示例所示:
<code class="language-javascript">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</code>
将全局对象作为参数传递给 IIFE 是一种常见模式,以便在函数内部访问它而无需使用 window
对象,这使得代码独立于浏览器环境。以下代码创建了一个变量 global
,无论您使用什么平台,它都将引用全局对象:
<code class="language-javascript">function showMessage(message) { setTimeout(function() { alert(message); }, 3000); } showMessage('Function called 3 seconds ago');</code>
这段代码在浏览器中(全局对象是 window
)或 Node.js 环境中(我们使用特殊变量 global
引用全局对象)都能工作。
IIFE 的一大好处是,使用它时,您不必担心用临时变量污染全局空间。您在 IIFE 内部定义的所有变量都将是局部的。让我们检查一下:
<code class="language-html"><!-- HTML --> <button id="btn">Click me</button> <!-- JavaScript --> function showMessage() { alert('Woohoo!'); } var el = document.getElementById("btn"); el.addEventListener("click", showMessage);</code>
在这个示例中,第一个 console.log()
语句工作正常,但第二个语句失败了,因为由于 IIFE,变量 today
和 currentTime
变成了局部变量。
我们已经知道闭包会保留对外部变量的引用,因此,它们会返回最新/更新的值。那么,您认为以下示例的输出是什么?
<code class="language-javascript">function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation("Paris"); // 输出:You are in Paris, France</code>
您可能期望水果的名称会以一秒钟的间隔一个接一个地打印出来。但是,实际上,输出是四次“undefined”。那么,问题出在哪里呢?
问题在于,在 console.log()
语句中,i
的值对于循环的每次迭代都等于 4。并且,由于我们在 fruits
数组中索引 4 处没有任何内容,因此输出为“undefined”。(记住,在JavaScript中,数组的索引从 0 开始。)当 i
等于 4 时,循环终止。
为了解决这个问题,我们需要为循环创建的每个函数提供一个新的作用域——这将捕获 i
变量的当前状态。我们通过在 IIFE 中关闭 setTimeout()
方法,并定义一个私有变量来保存 i
的当前副本,来做到这一点。
<code class="language-javascript">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</code>
我们还可以使用以下变体,它执行相同的任务:
<code class="language-javascript">function printLocation () { console.log("You are in " + city + ", " + country); }</code>
IIFE 通常用于创建作用域以封装模块。在模块内,存在一个自包含的私有作用域,可以防止意外修改。这种技术称为模块模式,是使用闭包管理作用域的强大示例,它在许多现代JavaScript库(例如jQuery和Underscore)中大量使用。
结论
本教程的目的是尽可能清晰简洁地介绍这些基本概念——作为一组简单的原则或规则。很好地理解它们是成为一名成功且高效的JavaScript开发人员的关键。
为了更详细和深入地解释此处介绍的主题,我建议您阅读 Kyle Simpson 的《你不知道JS:作用域与闭包》。
(后续内容,即FAQ部分,由于篇幅过长,已省略。如有需要,请提出具体问题。)
以上是揭开JavaScript关闭,回调和IIFES的神秘面纱的详细内容。更多信息请关注PHP中文网其他相关文章!