本文将深入探讨现代JavaScript开发中三个至关重要的概念:闭包、回调函数和立即执行函数表达式 (IIFE)。我们已详细了解变量作用域和提升,现在让我们完成探索之旅。
核心要点
- JavaScript闭包是能够访问其父作用域变量的函数,即使父函数已执行完毕,闭包仍然可以记住并操作这些变量。
- 回调函数是作为参数传递给其他函数的函数,这些函数随后在外部函数内执行,从而提供了一种延迟执行或维护异步操作顺序的方法。
- 立即执行函数表达式 (IIFE) 是在定义后立即执行的函数,用于保护变量的作用域并防止全局作用域污染。
- 闭包既可以读取也可以更新存储在其作用域中的变量,并且这些更新对任何访问这些变量的闭包都是可见的,这证明了闭包存储的是变量的引用,而不是值。
- 使用IIFE有助于在函数内创建私有作用域,从而更好地管理变量并防止外部访问这些变量。
- 这些概念(闭包、回调函数和IIFE)的组合为编写简洁、高效和安全的JavaScript代码提供了强大的工具,可以封装功能并避免全局作用域污染。
闭包
在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,您需要执行两个步骤:
- 您需要将整个函数括在括号中。顾名思义,IIFE 必须是函数表达式,而不是函数定义。因此,封闭括号的目的是将函数定义转换为表达式。这是因为在JavaScript中,括号中的所有内容都被视为表达式。
- 您需要在最后添加一对括号(变体 1),或者在闭合大括号之后添加一对括号(变体 2),这会导致函数立即执行。
还需要记住三件事:
首先,如果您将函数分配给变量,则不需要将整个函数括在括号中,因为它已经是表达式了:
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中文网其他相关文章!

从C/C 转向JavaScript需要适应动态类型、垃圾回收和异步编程等特点。1)C/C 是静态类型语言,需手动管理内存,而JavaScript是动态类型,垃圾回收自动处理。2)C/C 需编译成机器码,JavaScript则为解释型语言。3)JavaScript引入闭包、原型链和Promise等概念,增强了灵活性和异步编程能力。

不同JavaScript引擎在解析和执行JavaScript代码时,效果会有所不同,因为每个引擎的实现原理和优化策略各有差异。1.词法分析:将源码转换为词法单元。2.语法分析:生成抽象语法树。3.优化和编译:通过JIT编译器生成机器码。4.执行:运行机器码。V8引擎通过即时编译和隐藏类优化,SpiderMonkey使用类型推断系统,导致在相同代码上的性能表现不同。

JavaScript在现实世界中的应用包括服务器端编程、移动应用开发和物联网控制:1.通过Node.js实现服务器端编程,适用于高并发请求处理。2.通过ReactNative进行移动应用开发,支持跨平台部署。3.通过Johnny-Five库用于物联网设备控制,适用于硬件交互。

我使用您的日常技术工具构建了功能性的多租户SaaS应用程序(一个Edtech应用程序),您可以做同样的事情。 首先,什么是多租户SaaS应用程序? 多租户SaaS应用程序可让您从唱歌中为多个客户提供服务

本文展示了与许可证确保的后端的前端集成,并使用Next.js构建功能性Edtech SaaS应用程序。 前端获取用户权限以控制UI的可见性并确保API要求遵守角色库

JavaScript是现代Web开发的核心语言,因其多样性和灵活性而广泛应用。1)前端开发:通过DOM操作和现代框架(如React、Vue.js、Angular)构建动态网页和单页面应用。2)服务器端开发:Node.js利用非阻塞I/O模型处理高并发和实时应用。3)移动和桌面应用开发:通过ReactNative和Electron实现跨平台开发,提高开发效率。

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

DVWA
Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

WebStorm Mac版
好用的JavaScript开发工具

VSCode Windows 64位 下载
微软推出的免费、功能强大的一款IDE编辑器

mPDF
mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

PhpStorm Mac 版本
最新(2018.2.1 )专业的PHP集成开发工具