理解 JavaScript 闭包的简单指南

2025-06-07

理解 JavaScript 闭包的简单指南

目录

  1. 闭包简介
  2. 基本原则
  3. 作用域和作用域链
  4. 重新审视闭包
  5. 结束语

1. 闭包简介

闭包是 JavaScript 编程语言的一个非常强大的特性。

💡闭包是将一个函数与其周围状态(词法环境)的引用捆绑在一起(封装)而成的组合。换句话说,闭包允许你从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会在每次创建函数时创建,即在函数创建时创建 。MDN

上面这个绝妙的定义完美地解释了闭包。它实际上是 JavaScript 语言的一个特性,它不是我们编写的代码;它只是 JavaScript 语言的工作方式所决定的。因此,即使在父函数(外部函数)返回后,函数仍然能够访问其父函数(外部函数)的变量。

让我们通过下面的例子来进一步解释上述定义:
请运行下面的代码并考虑其结果。

function getBio(shortDevBio) { return function(devName) { console.log("Hi, my name is " + devName + " " + shortDevBio); } } const talkAboutDev = getBio("I am a developer, writer and instructor") talkAboutDev("Lawrence Eagles")

我们上面精心设计的小例子包含一个getBio函数,它接受一个简洁的开发者简历作为参数,并返回另一个接受开发者姓名作为参数的函数(匿名函数)。这个内部函数会将开发者的姓名和简历打印到控制台,以此来告诉我们开发者的信息。
需要注意的是,getBio函数返回的不是函数调用,而是匿名函数。这意味着,当调用getBio函数时,它会返回以下代码:

function(name) {
        console.log("Hi, my name is " + name + " " + shortDevBio);
   }
Enter fullscreen mode Exit fullscreen mode

因为这是一个匿名函数,所以我们将它赋值给变量talkAboutDev。然后,我们通过talkAboutDev变量调用这个匿名函数,该变量现在在内存中保存了对它的引用。
我已经在上一篇JavaScript 函数式编程系列文章中解释了为什么这样做。
如果您对此还不是很清楚,我建议您参考我关于 JavaScript 中的匿名函数和一等函数的文章进行快速复习。您可以在下面访问:

因此,当我们调用talKAboutDev函数时,它会告诉我们传递给getBio函数的开发者的简介。
这很令人费解❗

talkAboutDev函数是如何获取开发者的简历的,因为 getBio 函数在调用之前就已经返回了?

当你消化这个问题时,你可以再看一下代码:

function getBio(shortDevBio) {
    return function(devName) {
        console.log("Hi, my name is " + devName + " " + shortDevBio);
   }
}

const talkAboutDev = getBio("I am a developer, writer and instructor")
talkAboutDev("Lawrence Eagles") // returns "Hi, my name is Lawrence Eagles I am a developer, writer, and instructor"

// But how did it get the developer bio?
Enter fullscreen mode Exit fullscreen mode

💡这得益于闭包。内部函数 talkAboutDev 即使在返回后仍然可以访问外部函数 (getBio) 的 (自由) 变量。

上面的答案可能不太令人满意,尤其是如果你之前对 JavaScript 中的闭包还不是很了解的话。在接下来的章节中,我们将深入探讨 JavaScript 编程语言中这个臭名昭著、通常难以理解但又极其强大的特性。

充分理解 JavaScript 中的闭包非常重要,特别是当你想提高你的语言技能时。

为了完全理解 JavaScript 中的闭包,我们需要对一些关键概念有扎实的理解,这些概念是它的基本原则。
我们将在下一节中讨论这些内容。

2. 基本原则

第一节概述了闭包。虽然我们已经了解了它的实际应用,但仍有一些问题尚未解答。为了彻底理解它,我们需要了解在 JavaScript 中创建闭包时所涉及的关键概念。下面我们
将逐一讲解。

1.执行上下文。

💡 JavaScript 中的每段代码都在一个包装器中运行,这个包装器被称为执行上下文

当 JavaScript 程序运行时,会创建一个基本(全局)执行上下文,它包裹着所有代码。
如下图所示:

Alt 执行上下文

从我们的图中我们可以看到全局执行上下文是由全局对象,this变量,变量环境,外部环境组成的。

为了更好地理解闭包甚至 JavaScript 语言,我们需要了解所有这些以及它们在程序运行时如何交互。

全局对象

这是window对象。它代表浏览器的当前标签页。如果打开另一个标签页,则会获得一个单独的全局对象,因为这会创建一个单独的执行上下文。然而,在Node.js环境中,全局对象并非 window 对象。

💡在浏览器中,全局对象是 窗口 对象,但在 Node.js 中,全局对象被称为 全局 对象

请运行并考虑以下代码的结果:

console.log(this)

💡当 JavaScript 引擎首次运行你的代码时,会创建全局执行上下文。即使 .js 文件为空,全局执行上下文也会被创建。

上面的 runkit 程序是一个空的.js文件。注意,全局执行上下文仍然被创建,因此我们在控制台中获取了全局对象。需要注意的是,runkit 是一个 node.js 环境,因此全局对象被称为 global。
在控制台中 Alt 全局对象

this变量或关键字

这是一个特殊的 JavaScript 对象。我曾在 JavaScript 面向对象编程 (OOP) 系列的一篇文章中详细讨论过它。请阅读下文了解更多信息。


这里我们只想说,在全局层面上,this变量相当于全局对象。它指向全局对象。
可变环境

这指的是变量在内存中的位置以及它们之间的关系。每个执行上下文都有其自己的变量环境。对于全局执行上下文,变量环境就是全局对象。

外部环境

当我们在函数内部执行代码时,外部环境是该函数外部的代码。但在全局层面,外部环境为,因为外部没有任何内容。我们处于最外层

让我们通过一些例子来详细解释一下。

请检查下面的代码。
你期望以什么顺序看到这三个
console.log() 结果?

💡在你进行本练习时,需要注意的一点是,每当调用一个函数时,都会创建一个新的执行上下文,并将其添加到执行堆栈的顶部。此外,每当一个函数返回时,它的执行上下文都会从执行堆栈中移除。

function father() {
    child();
    let lastName = "Eagles"
    console.log(lastName)
}

function child() {
   let firstname = "Lawrence";
   console.log(firstname)
}

father();
var fullName = "Lawrence Eagles";
console.log(fullName);
Enter fullscreen mode Exit fullscreen mode

在我们在 runkit 上运行上面的示例之前,让我们深入了解一下 JavaScript 引擎如何执行此代码。

  • 首先创建全局执行上下文,并将所有这些函数和变量添加到内存中的某个位置(在全局执行上下文中,这就是全局变量)。

执行上下文的创建分为两个阶段:创建阶段和执行阶段。我在一篇旧文章中介绍过这个,您可以在下面访问。

  • 在全局执行上下文创建的执行阶段,father()函数会被调用,这将创建一个新的执行上下文,该上下文位于执行堆栈的顶部。该执行上下文中的代码(实际上是该函数代码块中的代码)将被执行。

  • child ()函数在执行函数代码块中的代码时被调用,并创建一个新的执行上下文,并将其放置在执行栈的顶部。此时,子函数
    的执行上下文(即位于执行栈顶部的执行上下文) 中的代码将被执行。

💡通常,如果在函数内调用一个函数,则会创建一个新的(内部函数的)执行上下文并将其放在执行堆栈的顶部,并且直到内部函数返回并且其执行上下文从执行堆栈中删除之前,父函数的代码将不会被执行。

  • 在子函数的执行上下文中执行代码期间字符串“Lawrence”被分配给firstName变量,并记录到控制台。

  • 子函数返回后,其执行上下文从执行栈中弹出(被移除)。父函数的执行上下文现在位于执行栈的顶部;因此其代码将继续执行

  • 接下来,将字符串“Eagles”赋值给变量lastName ,并将其打印到控制台。这标志着函数执行的结束;因此,它的执行上下文从执行堆栈中弹出,剩下的就是全局执行上下文。

  • 只有现在,全局执行上下文中剩余的代码才会被执行。字符串“Lawrence Eagles”现在被赋值给变量fullName,并且会被打印到控制台上。

根据上面的解释我们期望得到这样的结果:

// "Lawrence"
// "Eagles"
// "Lawrence Eagles"
Enter fullscreen mode Exit fullscreen mode

请运行并检查下面的代码。

function father() { child(); let lastName = "Eagles" console.log(lastName) } function child() { let firstname = "Lawrence"; console.log(firstname) } father(); var fullName = "Lawrence Eagles"; console.log(fullName);

3.作用域和作用域链

在本节中,我们将讨论作用域和作用域链,并通过代码示例详细阐述变量环境外部环境

请考虑以下代码。

💡需要注意的一个很好的提示是,当 JavaScript 引擎在执行上下文的变量环境中看不到变量时 ,它会转到外部环境中寻找它。

function logDevName() {
   console.log(devName)
}

function logDevName2() {
    var devName = "Lawrence Eagles"
    console.log(devName)
    logDevName()
}

var devName = "Brendan Eich"
console.log(devName)
logDevName2()
Enter fullscreen mode Exit fullscreen mode

你认为每次console.log()时 devName 变量的值应该是多少

为了回答这个问题,让我们来看看 JavaScript 引擎执行这段代码的方式。

  • 首先创建全局执行,并将所有这些函数和变量添加到内存中的某个位置(在全局执行上下文中,这就是全局变量)。

  • 在全局执行上下文创建的执行阶段,字符串“Brendan Eich”被分配给变量devName并记录到控制台。

  • 然后调用logDevName2函数,创建一个新的执行上下文并放在执行堆栈的顶部。

  • 在执行logDevName2函数时,字符串“Lawrence Eagles”被分配给变量devName,并记录到控制台;因此,此执行上下文中的devName为“Lawrence Eagles”。

  • 接下来,调用logDevName函数并创建一个新的执行上下文并将其放在执行堆栈的顶部。

  • 在此函数执行期间,变量devName被打印到控制台。但它并不在此局部作用域内,因为它不在此函数执行上下文的变量环境中(它未在此函数内声明)。

💡范围是可以找到变量的地方。

  • 因此,JavaScript 引擎会去其外部环境中查找该变量;在这种情况下,外部环境就是全局执行上下文。这是由logDevName函数的词法环境决定的。

💡词法环境指的是代码中物理写入的位置。JavaScript 引擎使用它来确定代码在内存中的位置以及它们之间的连接方式。在我们的示例中,logDevName 和 logDevName2 函数都位于全局执行上下文中;因此,它是这两个函数的外部环境,即使 logDevName 函数是在 logDevName2 函数内部调用的

  • 在全局执行上下文的变量环境中可以找到devName变量,它对应的是“Brendan Eich”,因此在控制台中打印出了字符串“Brendan Eich”。您可以再看一下下面的代码,希望您在 runkit 中运行代码查看结果时能够更好地理解。
function logDevName() { console.log(devName) } function logDevName2() { var devName = "Lawrence Eagles" console.log(devName) logDevName() } var devName = "Brendan Eich" console.log(devName) logDevName2()

💡 JavaScript 引擎使用对外部环境的引用,从函数的执行上下文跳转到其外部环境,并继续向上,直到到达全局执行上下文。这种指向不同外部环境的链接或引用的连接被称为作用域链。

4. 重新审视闭包

既然我们现在已经了解了掌握闭包概念所需的所有基本原则,让我们重新审视第一个例子并回答我们长期存在的问题。

请注意,当函数返回并且其执行上下文从执行堆栈中删除时,其内部函数仍然可以访问其变量环境中的变量,因为 JavaScript 引擎会沿作用域链进行搜索

function getBio(shortDevBio) { return function(devName) { console.log("Hi, my name is " + devName + " " + shortDevBio); } } const talkAboutDev = getBio("I am a developer, writer and instructor") talkAboutDev("Lawrence Eagles")

即使getBio函数返回,其执行上下文也已从执行堆栈中移除,内部函数仍然能够获取shortDevBio变量的值,因为其内部函数仍然持有对其变量环境的引用。因此,JavaScript 引擎能够在getBio函数的变量环境中继续沿作用域链向上搜索,找到(空闲)变量(例如shortDevBio )的位置。

因此,我们可以说内部函数的执行上下文包围了其外部变量。我们也可以说它包围了所有它应该可以访问的变量。这种现象被称为闭包。

它使一些最流行的 JavaScript 框架和库中使用的一些非常强大的 JavaScript 设计模式成为可能。

上面的代码可以用另一种语法重写。
请检查下面的代码:

function getBio(shortDevBio) {
    return function(devName) {
        console.log("Hi, my name is " + devName + " " + shortDevBio);
   }
}

// uses an alternate syntax to run both functions in one line.
const developerBio = getBio("I am a developer, writer and instructor.")("Lawrence Eagles")
console.log(developerBio)
Enter fullscreen mode Exit fullscreen mode

你认为 console.log() 会输出什么?

💡替代语法使用两个括号。第一个 () 只是调用外部 (getBio) 函数,由于它返回另一个函数,第二个 () 调用内部函数,因此我们得到相同的结果。

您可以在下面的runkit中运行代码:

function getBio(shortDevBio) { return function(devName) { console.log("Hi, my name is " + devName + " " + shortDevBio); } } // uses an alternate syntax to run both functions in one line. const developerBio = getBio("I am a developer, writer and instructor.")("Lawrence Eagles")

当我们使用 React -Redux 库与 React 和 Redux合作时,我们可以看到这种模式的实际作用

React-Redux 是一个将 React 和 Redux 连接在一起的库。它自称是 Redux 的官方 React 绑定,并由 Redux 团队维护。

以下是其官方文档中示例的摘录

export default connect(
  null,
  mapDispatchToProps
)(TodoApp)
Enter fullscreen mode Exit fullscreen mode

这里的细节超出了本文的讨论范围,但我只想指出connect函数是如何通过两个括号调用的。第一个括号接受nullmapDispatchToProps,而第二个括号接受TodoApp组件作为参数,然后导出结果。
这种模式得益于JavaScript 中的闭包

5. 结束语

这篇文章真的很长,如果您能读到这里,我深表感谢。
我希望您能看到我们这次长篇讨论的成果,并至少从本文中有所收获。如果是这样,我期待在下面的评论区听到您的意见、评论、问题或请求(如果您有任何不清楚的地方)。

文章来源:https://dev.to/lawrence_eagles/an-easy-guide-to-understanding-closures-in-javascript-4n5m
PREV
终端爱上指南
NEXT
如何正确地练习所学知识 刻意练习的特点