闭包是JavaScript的基本概念之一,给许多初学者带来了麻烦,这是每个JS程序员都应该了解和理解的。 对闭包有一个很好的了解,您可以编写更好,更有效和更干净的代码。 反过来,这将有助于您的专业发展。
该材料(我们今天出版的翻译)专门介绍闭包的内部机制及其在JavaScript程序中的工作原理。
什么是封包?
闭包是一种功能,即使该外部功能已完成其工作,也可以访问由外部功能相对于它形成的作用域。 这意味着闭包可以存储在外部函数中声明的变量以及传递给它的参数。 实际上,在进行闭包之前,我们将处理“词汇环境”的概念。
什么是词汇环境?
JavaScript中的术语“词汇环境”或“静态环境”是指根据变量,函数和对象在源代码中的物理位置来访问它们的能力。 考虑一个例子:
let a = 'global'; function outer() { let b = 'outer'; function inner() { let c = 'inner' console.log(c); // 'inner' console.log(b); // 'outer' console.log(a); // 'global' } console.log(a); // 'global' console.log(b); // 'outer' inner(); } outer(); console.log(a); // 'global'
在此,
inner()
函数可以访问在其自己的作用域,
outer()
函数的作用域和全局作用域中声明的变量。
outer()
函数可以访问在其自己的作用域和全局作用域中声明的变量。
上面代码的作用域链如下所示:
Global { outer { inner } }
请注意,
inner()
函数被
inner()
函数的词法环境包围,而后者又被全局作用域包围。 这就是为什么
inner()
函数可以访问在
outer()
函数和全局范围中声明的变量的原因。
闭包的实际例子
在拆解内部电路的复杂性之前,请考虑一些实际示例。
▍示例1
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
在这里,我们调用
person()
函数,该函数返回内部函数
displayName()
,并将此函数存储在变量
peter
。 此后,当我们调用
peter()
函数(相应的变量实际上存储对
displayName()
函数的引用)时,名称
Peter
将显示在控制台中。
同时,
displayName()
函数没有名为
name
的变量,因此我们可以得出结论,该函数可以以某种方式访问在其外部函数
person()
声明的变量,即使之后该功能如何工作。 也许是因为
displayName()
函数实际上是一个闭包。
▍示例2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
在这里,与前面的示例一样,我们将指向
getCounter()
函数返回的匿名内部函数的链接存储在变量
count
。 由于
count()
函数是一个闭包,因此即使在
getCounter()
函数完成其工作之后,它也可以访问
getCount()
函数的
counter
变量。
请注意,每次调用
count()
函数时,
counter
变量的值都不会重置为0。 似乎应该将其重置为0,就像调用常规函数时那样,但这不会发生。
就像这样工作,因为每次调用
count()
函数时,都会为其创建一个新的作用域,但是
getCounter()
函数只有一个作用域。 由于
counter
变量是在
getCounter()
函数的范围内声明的,因此在调用
count()
函数之间的值将被保存,而不会重置为0。
短路如何工作?
到目前为止,我们已经讨论了什么是闭包,并研究了实际示例。 现在,让我们讨论使它们起作用的内部JavaScript机制。
为了理解闭包,我们需要处理两个关键的JavaScript概念。 这是执行上下文和词汇环境。
context执行上下文
执行上下文是一个抽象环境,在其中计算和执行JavaScript代码。 当执行全局代码时,这会在全局执行上下文中发生。 功能代码在功能的上下文中执行。
在某个时间点,代码只能在一种执行上下文中执行(JavaScript是一种单线程编程语言)。 这些过程使用所谓的调用堆栈进行管理。
调用堆栈是根据LIFO原理(后进先出-后进先出)排列的数据结构。 新元素只能放置在堆栈的顶部,并且只能从其中删除元素。
当前执行上下文将始终位于堆栈的顶部,并且当当前函数退出时,其执行上下文将从堆栈中拉出,控制权将转移到执行上下文,该执行上下文位于调用堆栈中该函数的上下文下方。
考虑以下示例,以更好地了解执行上下文和调用堆栈是什么:
执行上下文示例执行此代码时,JavaScript引擎将创建用于执行全局代码的全局执行上下文,并且在遇到对
first()
函数的调用时,会为此函数创建一个新的执行上下文并将其放置在堆栈的顶部。
此代码的调用堆栈如下所示:
调用堆栈当
first()
函数的执行完成时,将从调用堆栈中检索其执行上下文,并将控制权转移到它下面的执行上下文,即全局上下文。 之后,将执行保留在全局范围内的代码。
▍词汇环境
每次JS引擎创建用于执行函数或全局代码的执行上下文时,它还会创建一个新的词法环境,用于存储在函数执行期间在该函数中声明的变量。
词汇环境是一种数据结构,用于存储有关标识符和变量的对应关系的信息。 在此,“标识符”是变量或函数的名称,“变量”是对对象(包括函数)的引用或原始类型的值。
词汇环境包含两个组件:
- 环境记录是存储变量和函数声明的位置。
- 对外部环境的引用-允许您访问外部(父)词法环境的链接。 这是为了了解闭包需要处理的最重要的组件。
从概念上讲,词汇环境如下所示:
lexicalEnvironment = { environmentRecord: { <identifier> : <value>, <identifier> : <value> } outer: < Reference to the parent lexical environment> }
看一下以下代码片段:
let a = 'Hello World!'; function first() { let b = 25; console.log('Inside first function'); } first(); console.log('Inside global execution context');
当JS引擎创建用于执行全局代码的全局执行上下文时,它还会创建一个新的词法环境,用于存储在全局范围内声明的变量和函数。 结果,全局范围的词法环境将如下所示:
globalLexicalEnvironment = { environmentRecord: { a : 'Hello World!', first : < reference to function object > } outer: null }
请注意,由于全局作用域没有外部词汇环境,因此对外部词汇环境(external)的引用设置为
null
。
当引擎为
first()
函数创建执行上下文时,它还会创建一个词法环境来存储在该函数执行期间在该函数中声明的变量。 结果,该函数的词法环境将如下所示:
functionLexicalEnvironment = { environmentRecord: { b : 25, } outer: <globalLexicalEnvironment> }
到函数的外部词法环境的链接设置为
<globalLexicalEnvironment>
,因为在源代码中,函数代码在全局范围内。
请注意,函数完成工作后,将从调用堆栈中检索其执行上下文,但是其词法环境可能会从内存中删除,或者可能保留在内存中。 这取决于在其他词汇环境中是否以链接到外部词汇环境的形式引用此词汇环境。
有关使用闭包的示例的详细分析
现在,我们已经掌握了执行上下文和词汇环境的知识,我们将返回到闭包并更深入地分析已经检查过的相同代码片段。
▍示例1
看一下以下代码片段:
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
当执行
person()
函数时,JS引擎为此函数创建一个新的执行上下文和一个新的词法环境。 完成工作后,该函数返回
displayName()
函数,对该函数的引用被写入变量
peter
。
她的词汇环境将如下所示:
personLexicalEnvironment = { environmentRecord: { name : 'Peter', displayName: < displayName function reference> } outer: <globalLexicalEnvironment> }
当
person()
函数退出时,其执行上下文将从堆栈中弹出。 但是它的词法环境仍保留在内存中,因为在其内部函数
displayName()
的词法环境中有指向它的链接。 结果,在此词法环境中声明的变量仍然可用。
当调用
peter()
函数(相应的变量存储对
displayName()
函数的引用)时,JS引擎为此函数创建新的执行上下文和新的词法环境。 这个词汇环境如下所示:
displayNameLexicalEnvironment = { environmentRecord: { } outer: <personLexicalEnvironment> }
displayName()
函数中没有变量,因此其环境记录将为空。 在执行此函数期间,JS引擎将尝试在该函数的词法环境中查找
name
变量。
由于无法在
displayName()
函数的词法环境中找到搜索,因此搜索将在外部词法环境(即仍在内存中的
person()
函数的词法环境
person()
中继续进行。 引擎在那里找到所需的变量,并在控制台中显示其值。
▍示例2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
getCounter()
函数的词法环境如下所示:
getCounterLexicalEnvironment = { environmentRecord: { counter: 0, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
该函数返回分配给
count
变量的匿名函数。
当执行
count()
函数时,其词法环境如下所示:
countLexicalEnvironment = { environmentRecord: { } outer: <getCountLexicalEnvironment> }
执行此功能时,系统将在其词法环境中查找
counter
变量。 同样,在这种情况下,函数环境记录为空,因此将在函数的外部词法环境中继续搜索变量。
引擎找到该变量,将其显示在控制台中,然后增加
counter
变量,该
counter
变量存储在
getCounter()
函数的词法环境中。
结果,在第一次调用
count()
函数之后,
getCounter()
函数的词法环境将如下所示:
getCounterLexicalEnvironment = { environmentRecord: { counter: 1, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
每次调用
count()
函数时,JavaScript引擎都会为此函数创建一个新的词汇环境,并递增
counter
变量,这将导致
getCounter()
函数的词汇环境发生变化。
总结
在本文中,我们讨论了闭包是什么,并整理了闭包所基于的底层JavaScript机制。 闭包是最重要的JavaScript基本概念之一,每个JS开发人员都应该理解它们。 了解闭包是编写有效和高质量应用程序的步骤之一。
亲爱的读者们! 如果您有JS开发经验,请与初学者分享使用闭包的实际示例。
