JavaScript 闭包

摘要: 在本教程中,您将学习 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 variableCode 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 JohnCode language: JavaScript (javascript)

greeting() 函数接受一个名为 message 的参数,并返回一个接受单个名为 name 的参数的函数。

返回的函数返回一个问候消息,该消息是 messagename 变量的组合。

greeting() 函数的行为类似于函数工厂。它创建 sayHi()sayHello() 函数,并分别使用 HiHello 消息。

sayHi()sayHello() 是闭包。它们共享相同的函数体,但存储不同的作用域。

sayHi() 闭包中,messageHi,而在 sayHello() 闭包中,messageHello

循环中的 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):4Code 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):3Code 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):3Code language: CSS (css)

总结

  • 词法作用域描述了 JavaScript 引擎如何使用变量在代码中的位置来确定该变量在何处可用。
  • 闭包是函数及其记住外部作用域中的变量的能力的结合。
本教程对您有帮助吗?