myfreax

JavaScript 闭包

您将了解 JavaScript 闭包以及如何更有效地在代码中使用闭包

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 参数。

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

greeting() 函数的行为类似于函数工厂。它使用相应的消息和来创建 sayHi()sayHello()运行。

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

sayHi() 闭包中,messageHi,而在 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 引擎如何使用变量的位置来确定该变量的可用位置。
  • 闭包是函数,闭包函数具有可以记住外部作用域变量的能力。

内容导航