将闭合想象成您下课后随身携带的背包。背包里有你在课堂上学到的所有笔记和材料。即使课程结束后,您仍然可以在需要时使用背包中的所有物品。类似地,闭包允许函数保留对其外部作用域的变量和参数的访问,即使在外部函数完成运行并且这些变量在该函数外部不再可访问之后也是如此。
上面的解释是描述闭包的典型方式,但是对于刚接触 JavaScript 的人来说它适合初学者吗?并不真地。当我第一次遇到它时,我也觉得很困惑。这就是为什么我写这篇文章是为了让闭包尽可能简单,让任何人都能理解。我们将首先介绍基础知识,然后再深入探讨该主题。
要理解什么是闭包,我们必须简要了解一下 JavaScript 中的作用域。范围是指代码不同部分中变量和函数的可访问性。它决定了我们可以在程序中访问某些变量或函数的位置。
作用域有两种主要类型:全局作用域和局部作用域。在全局作用域中声明的变量存在于任何函数或块之外,从而使它们可以在整个代码中访问。相反,在局部范围内(例如在函数或块内)声明的变量只能在该特定函数或块内访问。下面的代码说明了这一解释。
// GLOBAL SCOPE let myName = "John Doe"; function globalScope() { console.log(myName); } globalScope(); //Output John Doe console.log(myName); // Accessible here as well // LOCAL SCOPE function localScope() { let age = 30; console.log(age); } localScope(); //Output 30 console.log(age); //Output age is not defined (Not Accessible)
然而,JavaScript 使用了一个称为词法作用域的概念,这对于理解闭包的工作原理至关重要。词法作用域意味着变量的可访问性由编写代码时的结构决定。简单来说,这就像在说:“如果在函数内声明变量,则只有该函数及其中的任何内容都可以访问该变量。”{https://javascript.info/closure}。
为了更清楚地理解这一点,让我们看看 JavaScript 在幕后是如何工作的。 JavaScript 使用称为执行上下文的东西,它就像一个保存正在运行的代码的容器。它跟踪变量、函数以及当前正在运行的代码部分。当脚本启动时,将创建全局执行上下文 (GEC)。需要注意的是,程序中只有一个全局执行上下文。
上图代表了程序开始时的全局执行上下文。它由两个阶段组成:创建(或内存)阶段和执行(或代码)阶段。在创建阶段,变量和函数存储在内存中——变量被初始化为未定义,函数被完全存储。在执行阶段,JavaScript 逐行运行代码,为变量赋值并执行函数。
现在我们了解了 JavaScript 如何处理执行上下文和词法作用域,我们可以看到它如何直接与闭包联系起来。
当内部函数保留对其外部函数作用域中的变量的访问权限时,即使外部函数已完成执行,也会创建 JavaScript 中的闭包。这是可能的,因为内部函数保留了定义它的词法环境,允许它“记住”并使用外部作用域中的变量。
// GLOBAL SCOPE let myName = "John Doe"; function globalScope() { console.log(myName); } globalScope(); //Output John Doe console.log(myName); // Accessible here as well // LOCAL SCOPE function localScope() { let age = 30; console.log(age); } localScope(); //Output 30 console.log(age); //Output age is not defined (Not Accessible)
这是有关上述代码如何工作的指南。每当我们调用一个函数时,JavaScript 引擎都会创建一个特定于该函数的函数执行上下文 (FEC),该函数执行上下文是在全局执行上下文 (GEC) 内创建的。与 GEC 不同,一个程序中可以有多个 FEC。每个 FEC 都会经历自己的创建和执行阶段,并拥有自己的变量和词法环境。词法环境使函数能够从其外部作用域访问变量。
当outerFunction被调用时,会创建一个新的FEC,在outerFunction内部,我们定义innerFunction,由于词法作用域,它可以访问outerVariable。在outerFunction返回后,outerFunction的执行上下文将从调用堆栈中删除,但innerFunction由于闭包而保留对outerVariable的访问。因此,当我们稍后调用closureExample()时,即使outerFunction已经完成,它仍然可以记录outerVariable。
让我们看一下以下示例:
Let’s look at the example below: function outerFunction() { let outerVariable = 'I am John Doe'; return function innerFunction() { console.log(outerVariable); }; } const closureExample = outerFunction(); closureExample(); // Outputs: "I am John Doe"
你认为这段代码的输出会是什么?你们中的许多人可能猜到了 5,但这真的是正确的输出吗?事实上,不,原因如下。函数 y() 引用变量 a,而不是其初始值。当 z() 被调用时,由于返回内部函数之前进行了更新,它会记录 a 的当前值,即 50。让我们探讨另一个例子:
function x(){ let a = 5 function y(){ console.log(a) } a = 50 return y; } let z = x(); console.log(z) z();
代码展示了闭包的威力。即使在最里面的函数 z() 中,它仍然可以从其父作用域访问变量。如果我们检查浏览器并检查 Sources 选项卡,我们可以看到 x 和 y 上都形成了闭包,这允许 z() 从其父上下文访问 a 和 b。
闭包在 JavaScript 中提供了多种优势,特别是在编写更灵活、模块化和可维护的代码时。以下是一些主要优点:
1。回调函数: 闭包在处理异步编程时非常强大,例如回调、事件监听器和 Promise。即使在外部函数完成后,它们也允许回调函数保持对外部函数变量的访问。
// GLOBAL SCOPE let myName = "John Doe"; function globalScope() { console.log(myName); } globalScope(); //Output John Doe console.log(myName); // Accessible here as well // LOCAL SCOPE function localScope() { let age = 30; console.log(age); } localScope(); //Output 30 console.log(age); //Output age is not defined (Not Accessible)
2。模块化和可维护性: 闭包允许开发人员编写更小、可重用的代码块,从而鼓励模块化。由于闭包可以在函数调用之间保留变量,因此减少了对重复逻辑的需求并提高了可维护性。
3。避免全局变量: 闭包有助于减少对全局变量的需求,从而避免潜在的命名冲突并保持全局命名空间干净。通过使用闭包,您可以将数据存储在函数范围内而不是全局。
闭包是 JavaScript 中一个强大的概念,它允许函数记住和访问其外部作用域中的变量,甚至在函数执行之后也是如此,从而扩展了函数的功能。此功能在创建更加模块化、灵活且高效的代码方面发挥着重要作用,特别是在处理异步任务、回调和事件侦听器时。虽然闭包一开始可能看起来很复杂,但掌握它们将使您能够编写更复杂和优化的 JavaScript。当您继续练习时,您将发现闭包如何帮助编写更干净、更易于维护的应用程序。不断尝试,很快闭包将成为你的 JavaScript 工具箱的自然组成部分。
以上是JavaScript 闭包的魔力:清晰易懂的指南的详细内容。更多信息请关注PHP中文网其他相关文章!