摘要: 在本教程中,您将学习 JavaScript 闭包以及如何在代码中更有效地使用闭包。
JavaScript 闭包简介
在 JavaScript 中,闭包是一个 函数,它从其内部作用域引用外部作用域中的变量。闭包在其内部作用域中保留外部作用域。
要理解闭包,您首先需要了解词法作用域是如何工作的。
词法作用域
词法作用域通过在源代码中声明的变量位置定义 变量的作用域。例如
let name = 'John';
function greeting() {
let message = 'Hi';
console.log(message + ' '+ name);
}
Code language: JavaScript (javascript)
在这个例子中
- 变量
name
是一个全局变量。它可以在任何地方访问,包括在greeting()
函数内部。 - 变量
message
是一个局部变量,只能在greeting()
函数内部访问。
如果您尝试在 greeting()
函数外部访问 message
变量,您将收到错误。
因此,JavaScript 引擎使用作用域来管理变量的可访问性。
根据词法作用域,作用域可以嵌套,内部函数可以访问在其外部作用域中声明的变量。例如
function greeting() {
let message = 'Hi';
function sayHi() {
console.log(message);
}
sayHi();
}
greeting();
Code language: JavaScript (javascript)
greeting()
函数创建一个名为 message
的局部变量和一个名为 sayHi()
的函数。
sayHi()
是内部函数,只能在 greeting()
函数主体内部使用。
sayHi()
函数可以访问外部函数的变量,例如 greeting()
函数的 message
变量。
在 greeting()
函数内部,我们调用 sayHi()
函数来显示消息 Hi
。
JavaScript 闭包
让我们修改 greeting()
函数
function greeting() {
let message = 'Hi';
function sayHi() {
console.log(message);
}
return sayHi;
}
let hi = greeting();
hi(); // still can access the message variable
Code language: JavaScript (javascript)
现在,我们不再在 greeting()
函数内部执行 sayHi()
函数,而是让 greeting()
函数返回 sayHi()
函数对象。
请注意,函数是 JavaScript 中的一等公民,因此您可以从另一个函数中返回函数。
在 greeting()
函数外部,我们将 hi
变量的值设置为 greeting()
函数返回的值,这是一个对 sayHi()
函数的引用。
然后我们使用该函数的引用执行 sayHi()
函数:hi()
。如果您运行代码,您将获得与上面相同的效果。
然而,这里有趣的是,通常情况下,局部变量只在函数执行期间存在。
这意味着当 greeting()
函数完成执行后,message
变量将不再可访问。
在这种情况下,我们执行了引用 sayHi()
函数的 hi()
函数,message
变量仍然存在。
这种神奇的效果就是 **闭包**。换句话说,sayHi()
函数就是一个闭包。
闭包是一个保留外部作用域在其内部作用域中的函数。
更多 JavaScript 闭包示例
以下示例说明了闭包的更实际的示例。
function greeting(message) {
return function(name){
return message + ' ' + name;
}
}
let sayHi = greeting('Hi');
let sayHello = greeting('Hello');
console.log(sayHi('John')); // Hi John
console.log(sayHello('John')); // Hello John
Code language: JavaScript (javascript)
greeting()
函数接受一个名为 message
的参数,并返回一个接受单个名为 name
的参数的函数。
返回的函数返回一个问候消息,该消息是 message
和 name
变量的组合。
greeting()
函数的行为类似于函数工厂。它创建 sayHi()
和 sayHello()
函数,并分别使用 Hi
和 Hello
消息。
sayHi()
和 sayHello()
是闭包。它们共享相同的函数体,但存储不同的作用域。
在 sayHi()
闭包中,message
为 Hi
,而在 sayHello()
闭包中,message
为 Hello
。
循环中的 JavaScript 闭包
请考虑以下示例
for (var index = 1; index <= 3; index++) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
}
Code language: JavaScript (javascript)
输出
after 4 second(s):4
after 4 second(s):4
after 4 second(s):4
Code language: CSS (css)
代码显示相同的消息。
我们在循环中想要做的是在每次迭代时复制 i
的值,以便在 1、2 和 3 秒后显示消息。
您看到 4 秒后显示相同消息的原因是,传递给 setTimeout()
的回调是一个闭包。它会记住循环最后一次迭代时的 i
值,即 4。
此外,由 for 循环 创建的所有三个闭包共享相同的作用域,并访问相同的 i
值。
要解决此问题,您需要在循环的每次迭代中创建一个新的闭包作用域。
有两种流行的解决方案:IIFE 和 let
关键字。
1) 使用 IIFE 解决方案
在此解决方案中,您使用 立即调用的函数表达式 (IIFE),因为 IIFE 通过声明一个函数并立即执行它来创建一个新的作用域。
for (var index = 1; index <= 3; index++) {
(function (index) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
})(index);
}
Code language: JavaScript (javascript)
输出
after 1 second(s):1
after 2 second(s):2
after 3 second(s):3
Code language: CSS (css)
2) 在 ES6 中使用 let 关键字
在 ES6 中,您可以使用 let
关键字来声明一个块级作用域变量。
如果您在 for 循环 中使用 let
关键字,它将在每次迭代中创建一个新的词法作用域。换句话说,您将在每次迭代中获得一个新的 index
变量。
此外,新的词法作用域链接到上一个作用域,以便将 index
的上一个值从上一个作用域复制到新作用域。
for (let index = 1; index <= 3; index++) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
}
Code language: JavaScript (javascript)
输出
after 1 second(s):1
after 2 second(s):2
after 3 second(s):3
Code language: CSS (css)
总结
- 词法作用域描述了 JavaScript 引擎如何使用变量在代码中的位置来确定该变量在何处可用。
- 闭包是函数及其记住外部作用域中的变量的能力的结合。