JavaScript闭包、JavaScript回调 和 JavaScript IIFE,是JavaScript开发中三个最重要和最常用的概念,闭包是任何函数,函数可以作为参数传递给其他函数,也可以由其他函数返回。立即调用的函数表达式,或 IIFE,是一个在创建后立即执行的函数表达式(命名或匿名)。
JavaScript闭包
在 JavaScript 中,闭包是任何函数,即使在父级返回后,它仍保留对其父级作用域中变量的引用。
这意味着几乎任何函数都可以被视为闭包,因为正如我们在本教程第一部分的变量作用域部分中了解到的那样,函数可以引用或访问 –
- 其自身函数范围内的任何变量和参数
- 外部(父)函数的任何变量和参数
- 来自全局范围的任何变量。
因此,您很可能已经在不知情的情况下使用了闭包。但我们的目标不仅仅是使用它们——而是理解它们。如果我们不了解它们的工作原理,就无法正确使用它们。出于这个原因,我们将把上面的闭包定义分成三个易于理解的点。
要点1: 可以引用当前函数外定义的变量。
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation ("Paris"); // output: You are in Paris, France
在此代码示例中,printLocation()
函数引用封闭(父)函数的country
变量和参数。结果是调用when时,成功使用前者的变量和参数输出“你在法国巴黎”。city
setLocation()
setLocation()
printLocation()
要点 2: 即使在外部函数返回之后,内部函数也可以引用在外部函数中定义的变量。
function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } return printLocation; } var currentLocation = setLocation ("Paris"); currentLocation(); // output: You are in Paris, France
这与第一个示例几乎相同,只是这次是在外部函数内部printLocation()
返回setLocation()
,而不是立即调用。所以,的值currentLocation
是内部printLocation()
函数。
如果我们这样提醒currentLocation
————alert(currentLocation);
我们将得到以下输出:
function printLocation () { console.log("You are in " + city + ", " + country); }
如我们所见,printLocation()
在其词法范围之外执行。看起来它setLocation()
已经消失了,但printLocation()
仍然可以访问并“记住”它的变量 ( country
) 和参数 ( city
)。
闭包(内部函数)能够记住它周围的范围(外部函数),即使它是在其词法范围之外执行的。因此,您可以稍后在您的程序中随时调用它。
第 3 点: 内部函数通过引用而不是值存储其外部函数的变量。
function cityLocation() { var city = "Paris"; return { get: function() { console.log(city); }, set: function(newCity) { city = newCity; } }; } var myLocation = cityLocation(); myLocation.get(); // output: Paris myLocation.set('Sydney'); myLocation.get(); // output
这里cityLocation()
返回一个包含两个闭包的对象——get()
和set()
——它们都引用外部变量city
。get()
获取 的当前值city
,同时set()
更新它。当myLocation.get()
第二次被调用时,它输出更新的(当前)值city
——“Sydney”——而不是默认的“Paris”。
因此,闭包可以读取和更新它们存储的变量,并且更新对任何有权访问它们的闭包都是可见的。这意味着闭包存储对其外部变量的引用,而不是复制它们的值。这是需要记住的非常重要的一点,因为不知道它会导致一些难以发现的逻辑错误——正如我们将在“立即调用的函数表达式 (IIFE)”部分中看到的那样。
闭包的一个有趣特性是闭包中的变量会自动隐藏。闭包将数据存储在它们封闭的变量中,而不提供对它们的直接访问。改变这些变量的唯一方法是间接提供对它们的访问。例如,在最后一段代码中,我们看到我们city
只能通过使用get()
和set()
闭包间接地修改变量。
我们可以利用这种行为将私有数据存储在对象中。我们可以将数据作为变量存储在构造函数中,而不是将数据存储为对象的属性,然后使用闭包作为引用这些变量的方法。
如您所见,闭包没有任何神秘或深奥的东西——只需记住三个简单的要点。
JavaScipt回调
在 JavaScript 中,函数是一流的对象。这一事实的后果之一是函数可以作为参数传递给其他函数,也可以由其他
函数返回。
将其他函数作为参数或返回函数作为其结果的函数称为高阶函数,而作为参数传递的函数称为回调函数。之所以命名为“回调”,是因为在某个时间点它会被高阶函数“回调”。
回调有许多日常用法。其中之一是当我们使用浏览器对象的setTimeout()
和方法时——接受和执行回调的方法:setInterval()
window
function showMessage(message){ setTimeout(function(){ alert(message); }, 3000); } showMessage('Function called 3 seconds ago');
另一个例子是当我们将事件侦听器附加到页面上的元素时。通过这样做,我们实际上提供了一个指向回调函数的指针,该回调函数将在事件发生时被调用。
// HTML <button id='btn'>Click me</button> // JavaScript function showMessage(){ alert('Woohoo!'); } var el = document.getElementById("btn"); el.addEventListener("click", showMessage);
了解高阶函数和回调如何工作的最简单方法是创建您自己的函数。那么,让我们现在创建一个:
function fullName(firstName, lastName, callback){ console.log("My name is " + firstName + " " + lastName); callback(lastName); } var greeting = function(ln){ console.log('Welcome Mr. ' + ln); }; fullName("Jackie", "Chan", greeting);
在这里,我们创建了一个函数fullName()
,它接受三个参数——两个用于名字和姓氏,一个用于回调函数。然后,在console.log()
语句之后,我们放置了一个函数调用,它将触发实际的回调函数——greeting()
定义在fullName()
. 最后,我们调用fullName()
, 其中greeting()
作为变量传递 –不带括号– 因为我们不希望它立即执行,而只是想指向它以供以后使用fullName()
。
我们传递的是函数定义,而不是函数调用。这可以防止立即执行回调,这不是回调背后的想法。作为函数定义传递,它们可以在包含函数中的任何时间和任何点执行。此外,因为回调的行为就好像它们实际上被放置在该函数内一样,它们实际上是闭包:它们可以访问包含函数的变量和参数,甚至是全局范围内的变量。
回调可以是前面示例中所示的现有函数,也可以是我们在调用高阶函数时创建的匿名函数,如下例所示:
function fullName(firstName, lastName, callback){ console.log("My name is " + firstName + " " + lastName); callback(lastName); } fullName("Jackie", "Chan", function(ln){console.log('Welcome Mr. ' + ln);});
回调在 JavaScript 库中大量使用,以提供通用性和可重用性。它们允许轻松定制和/或扩展库方法。此外,代码更易于维护,并且更加简洁和可读。每次您需要将不必要的重复代码模式转换为更抽象/通用的函数时,回调都会来救援。
假设我们需要两个函数——一个打印已发表文章的信息,另一个打印已发送消息的信息。我们创建了它们,但我们注意到我们的某些逻辑部分在两个函数中都重复了。我们知道,在不同的地方拥有一段相同的代码是不必要的,而且很难维护。那么,解决方案是什么?让我们在下一个例子中说明它:
function publish(item, author, callback){ // Generic function with common data console.log(item); var date = new Date(); callback(author, date); } function messages(author, time){ // Callback function with specific data var sendTime = time.toLocaleTimeString(); console.log("Sent from " + author + " at " + sendTime); } function articles(author, date){ // Callback function with specific data var pubDate = date.toDateString(); console.log("Written by " + author); console.log("Published " + pubDate); } publish("How are you?", "Monique", messages); publish("10 Tips for JavaScript Developers", "Jane Doe", articles);
我们在这里所做的是将重复的代码模式 (console.log(item)
和var date = new Date()
) 放入一个单独的通用函数 ( publish()
) 中,并仅将特定数据留在其他函数中——现在是回调。这样,我们就可以使用同一个功能打印各种相关事物的信息——消息、文章、书籍、杂志等等。您唯一需要做的就是为每种类型创建一个专门的回调函数,并将其作为参数传递给该publish()
函数。
立即调用函数表达式 (IIFE)
立即调用的函数表达式,或 IIFE(发音为“iffy”),是一个在创建后立即执行的函数表达式(命名或匿名)。
此模式有两种略有不同的语法变体:
/ variant 1 (function () { alert('Woohoo!'); })(); // variant 2 (function () { alert('Woohoo!'); }());
要将常规函数转换为 IIFE,您需要执行两个步骤:
- 您需要将整个函数括在括号中。顾名思义,IIFE 必须是函数表达式,而不是函数定义。因此,括号的目的是将函数定义转换为表达式。这是因为在 JavaScript 中,括号中的所有内容都被视为表达式。
- 您需要在最后(变体 1)或右花括号(变体 2)之后添加一对圆括号,这会导致函数立即执行。
另外还有三点需要注意:
首先,如果将函数分配给变量,则不需要将整个函数括在括号中,因为它已经是一个表达式:
var sayWoohoo = function () { alert('Woohoo!'); }();
其次,在 IIFE 的末尾需要一个分号,否则您的代码可能无法正常运行。
第三,您可以将参数传递给 IIFE(毕竟它是一个函数),如以下示例所示:
(function (name, profession) { console.log("My name is " + name + ". I'm an " + profession + "."); })("Jackie Chan", "actor"); // output: My name is Jackie Chan. I'm an actor.
将全局对象作为参数传递给 IIFE 是一种常见的模式,这样它就可以在函数内部访问而无需使用该window
对象,这使得代码独立于浏览器环境。以下代码创建一个变量global
,无论您在哪个平台上工作,该变量都将引用全局对象:
(function (global) { // access the global object via 'global' })(this); </code></pre> <p>This code will work both in the browser (where the global object is <code>window</code>), or in a Node.js environment (where we refer to the global object with the special variable <code>global</code>). </p> <p>One of the great benefits of an IIFE is that, when using it, you don’t have to worry about polluting the global space with temporary variables. All the variables you define inside an IIFE will be local. Let’s check this out:</p> (function(){ var today = new Date(); var currentTime = today.toLocaleTimeString(); console.log(currentTime); // output: the current local time (e.g. 7:08:52 PM) })(); console.log(currentTime); // output: undefined
在此示例中,第一个console.log()
语句工作正常,但第二个语句失败,因为变量today
和变量currentTime
由于 IIFE 而成为局部变量。
我们已经知道闭包保留对外部变量的引用,因此它们返回最新/更新的值。那么,您认为以下示例的输出是什么?
function printFruits(fruits){ for (var i = 0; i < fruits.length; i++) { setTimeout( function(){ console.log( fruits[i] ); }, i * 1000 ); } } printFruits(["Lemon", "Orange", "Mango", "Banana"]);
您可能已经预料到水果的名称会以一秒的间隔一个接一个地打印出来。但是,实际上,输出是四次“未定义”。那么,问题在哪里?
i
要注意的是,语句内的值console.log()
对于循环的每次迭代都等于 4。而且,由于我们的 fruits 数组中索引 4 处没有任何内容,因此输出为“未定义”。(请记住,在 JavaScript 中,数组的索引从 0 开始。)循环在i < fruits.length
返回时终止false
。因此,在循环结束时, 的值为i
4。该变量的最新版本用于循环生成的所有函数。所有这一切的发生是因为闭包链接到变量本身,而不是它们的值。
为了解决这个问题,我们需要为循环创建的每个函数提供一个新的作用域,它将捕获i
变量的当前状态。我们通过关闭setTimeout()
IIFE 中的方法,并定义一个私有变量来保存i
.
function printFruits(fruits){ for (var i = 0; i < fruits.length; i++) { (function(){ var current = i; // define new variable that will hold the current value of "i" setTimeout( function(){ console.log( fruits[current] ); // this time the value of "current" will be different for each iteration }, current * 1000 ); })(); } } printFruits(["Lemon", "Orange", "Mango", "Banana"]);
我们还可以使用以下变体,它完成相同的工作:
function printFruits(fruits){ for (var i = 0; i < fruits.length; i++) { (function(current){ setTimeout( function(){ console.log( fruits[current] ); }, current * 1000 ); })( i ); } } printFruits(["Lemon", "Orange", "Mango", "Banana"]);
IIFE 通常用于创建范围以封装模块。在模块中有一个私有范围,它是独立的并且可以防止不必要的或意外的修改。这种称为模块模式的技术是使用闭包管理范围的有力示例,并且在许多现代 JavaScript 库(例如 jQuery 和 Underscore)中大量使用。
结论
本教程的目的是尽可能清晰简洁地介绍这些基本概念——作为一组简单的原则或规则。很好地理解它们是成为一名成功且富有成效的 JavaScript 开发人员的关键。