JavaScript 闭包
您将了解 JavaScript 闭包以及如何更有效地在代码中使用闭包
在本教程中,您将了解 JavaScript 闭包以及如何更有效地在代码中使用闭包。
JavaScript 闭包简介
在 JavaScript ,闭包是一个函数,它从其内部作用域引用外部作用域中的变量。闭包将外部作用域保留在其内部作用域内。
要理解闭包,您需要先了解词法作用域是如何工作的。
词法作用域
词法作用域定义了变量的作用域,而变量的作用域是通过变量在源代码声明的位置来决定。例如:
let name = 'John';
function greeting() {
let message = 'Hi';
console.log(message + ' '+ name);
}
在这个例子中:
name
变量是一个全局变量。它可以从任何地方访问,包括在greeting()
函数内。message
变量是一个局部变量,只能在greeting()
函数内访问。
如果你试图在 greeting()
函数外访问的 message
变量,你会得到一个错误。所以 JavaScript 引擎使用作用域来管理变量的可访问性。
根据词法作用域,作用域可以嵌套,内部函数可以访问在其外部作用域声明的变量。例如:
function greeting() {
let message = 'Hi';
function sayHi() {
console.log(message);
}
sayHi();
}
greeting();
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
现在,greeting()
函数返回 sayHi()
函数对象,而不是在函数 greeting()
内部执行 sayHi()
函数。
请注意,函数是 JavaScript 一等公民,因此,您可以从另一个函数返回一个函数。
在 greeting()
函数之外,我们将 greeting()
函数返回的值赋给变量 hi
,这是 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
greeting()
函数接受一个参数 message
并返回一个函数,该函数接受一个 name
参数。
返回的函数返回一条问候消息,它是 message
和 name
变量的组合。
greeting()
函数的行为类似于函数工厂。它使用相应的消息和来创建 sayHi()
和 sayHello()
运行。
sayHello()
和 sayHi()
是闭包。它们共享相同的函数体,但存储者不同的作用域。
在 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);
}
输出:
after 4 second(s):4
after 4 second(s):4
after 4 second(s):4
代码显示相同的消息。
我们想在循环迭代中每次复制 i
的值,以便在 1、2、3 秒后显示一条消息。
您在 4 秒后看到相同消息的原因是 setTimeout()
回调函数是一个闭包。i
它会记住循环最后一次迭代的值,即 4。
此外,for 循环创建所有三个闭包共享相同的全局作用域并访问相同的 i
值。要解决此问题,您需要在循环的每次迭代中创建一个新的闭包作用域。
有两种流行的解决方案:IIFE 和 let
关键词。
使用 IIFE 解决方案
在此解决方案中,您使用一个立即调用函数表达式(也称为 IIFE),因为 IIFE 通过声明一个函数并立即执行它来创建一个新的作用域。
for (var index = 1; index <= 3; index++) {
(function (index) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
})(index);
}
输出
after 1 second(s):1
after 2 second(s):2
after 3 second(s):3
在 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);
}
输出
after 1 second(s):1
after 2 second(s):2
after 3 second(s):3
结论
- 词法作用域描述 JavaScript 引擎如何使用变量的位置来确定该变量的可用位置。
- 闭包是函数,闭包函数具有可以记住外部作用域变量的能力。