跳转到主要内容
myfreax
返回

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

返回的函数返回一条问候消息,它是 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 引擎如何使用变量的位置来确定该变量的可用位置。
  • 闭包是函数,闭包函数具有可以记住外部作用域变量的能力。
myfreax

  1. 暂无评论,快来抢沙发吧!