闭包的示例
Closures are a fundamental JavaScript concept that every serious programmer should know inside and out.
闭包是一个基本JavaScript概念,每个认真的程序员都应该了解内在和外在。
The Internet is packed with great explanations of “what” closures are, but few deep-dive into the “why” side of things.
互联网上充斥着对“什么”闭包的详尽解释,但很少深入探讨事物“为什么”的一面。
I find that understanding the internals ultimately gives developers a stronger grasp of their tools, so this post will be dedicated to the nuts and bolts of how and why closures work the way they do.
我发现,了解内部结构最终可以使开发人员对他们的工具有更深入的了解,因此,本篇文章将专门介绍闭包如何以及为何以其工作方式运行。
Hopefully you’ll walk away better equipped to take advantage of closures in your day-to-day work. Let’s get started!
希望您能走得更好,在日常工作中充分利用封闭的优势。 让我们开始吧!
什么是封包? (What is a closure?)
Closures are an extremely powerful property of JavaScript (and most programming languages). As defined on MDN:
闭包是JavaScript(和大多数编程语言)的一种极其强大的属性。 根据MDN的定义:
Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure ‘remembers’ the environment in which it was created.
闭包是引用独立(自由)变量的 函数 。 换句话说,闭包中定义的函数“记住”了创建它的环境 。
Note: Free variables are variables that are neither locally declared nor passed as parameter.
注意:自由变量是既不在本地声明也不作为参数传递的变量。
Let’s look at some examples:
让我们看一些例子:
范例1: (Example 1:)
function numberGenerator() {
// Local “free” variable that ends up within the closure
var num = 1;
function checkNumber() {
console.log(num);
}
num++;
return checkNumber;
}
var number = numberGenerator();
number(); // 2
In the example above, the function numberGenerator creates a local “free” variable num (a number) and checkNumber (a function which prints num to the console).
在上面的示例中,函数numberGenerator创建一个本地“免费”变量num (一个数字)和checkNumber (一个将num打印到控制台的函数)。
The function checkNumber doesn’t have any local variables of its own — however, it does have access to the variables within the outer function, numberGenerator, because of a closure.
函数checkNumber本身没有任何局部变量-但是,由于有闭包,它确实可以访问外部函数numberGenerator中的变量。
Therefore, it can use the variable num declared in numberGenerator to successfully log it to the console even after numberGenerator has returned.
因此, 即使在 numberGenerator返回之后 ,它也可以使用numberGenerator中声明的变量num成功地将其记录到控制台。
范例2: (Example 2:)
In this example we’ll demonstrate that a closure contains any and all local variables that were declared inside the outer enclosing function.
在此示例中,我们将演示闭包包含在外部封装函数中声明的所有局部变量。
function sayHello() {
var say = function() { console.log(hello); }
// Local variable that ends up within the closure
var hello = 'Hello, world!';
return say;
}
var sayHelloClosure = sayHello();
sayHelloClosure(); // ‘Hello, world!’
Notice how the variable hello is defined after the anonymous function — but can still access the hello variable. This is because the hello variable has already been defined in the function “scope” at the time of creation, making it available when the anonymous function is finally executed.
请注意,如何在匿名函数之后定义变量hello ,但仍可以访问hello变量。 这是因为hello变量在创建时已在函数“作用域”中定义,从而使该变量在匿名函数最终执行时可用。
(Don’t worry, I’ll explain what “scope” means later in the post. For now, just roll with it!)
(不用担心,我将在后面的文章中解释“作用域”的含义。现在,只需滚动一下即可!)
了解高级 (Understanding the High Level)
These examples illustrated “what” closures are on a high level. The general theme is this: we have access to variables defined in enclosing function(s) even after the enclosing function which defines these variables has returned.
这些示例说明了“什么”关闭。 总体主题是这样的: 即使在定义了这些变量的封闭函数返回之后,我们也可以访问在封闭函数中定义的变量 。
Clearly, something is happening in the background that allows those variables to still be accessible long after the enclosing function that defined them has returned.
显然,在后台发生了一些事情,允许在定义它们的封闭函数返回后很长时间仍可以访问这些变量。
To understand how this is possible, we’ll need to touch on a few related concepts — starting 3000 feet up and slowly climbing our way back down to the land of closures. Let’s start with the overarching context within which a function is run, known as “Execution context”.
要了解这是如何实现的,我们需要涉及一些相关概念-从3000英尺开始,然后慢慢爬回封闭区域。 让我们从运行函数的总体上下文开始,称为“执行上下文” 。
执行上下文 (Execution Context)
Execution context is an abstract concept used by the ECMAScript specification to track the runtime evaluation of code. This can be the global context in which your code is first executed or when the flow of execution enters a function body.
执行上下文是ECMAScript规范用于以下目的的抽象概念: 跟踪代码的运行时评估。 这可以是在其中首次执行代码或执行流进入函数主体时的全局上下文。
At any point in time, there can only be one execution context running. That’s why JavaScript is “single threaded,” meaning only one command can be processed at a time.
在任何时间点,都只能运行一个执行上下文。 这就是JavaScript是“单线程”的原因,这意味着一次只能处理一个命令。
Typically, browsers maintain this execution context using a “stack.” A stack is a Last In First Out (LIFO) data structure, meaning the last thing that you pushed onto the stack is the first thing that gets popped off it. (This is because we can only insert or delete elements at the top of the stack.)
通常,浏览器使用“堆栈”维护此执行上下文。 堆栈是后进先出(LIFO)数据结构,这意味着您压入堆栈的最后一件事是从堆栈弹出的第一件事。 (这是因为我们只能在堆栈顶部插入或删除元素。)
The current or “running” execution context is always the top item in the stack. It gets popped off the top when the code in the running execution context has been completely evaluated, allowing the next top item to take over as running execution context.
当前或“正在运行”的执行上下文始终是堆栈中的第一项。 当正在运行的执行上下文中的代码已被完全评估时,它会从顶部弹出,从而允许下一个顶级项接管运行的执行上下文。
Moreover, just because an execution context is running doesn’t mean that it has to finish running before a different execution context can run.
而且,仅仅因为执行上下文正在运行并不意味着它必须在其他执行上下文可以运行之前完成运行。
There are times when the running execution context is suspended and a different execution context becomes the running execution context. The suspended execution context might then at a later point pick back up where it left off.
有时会暂停正在运行的执行上下文,而另一个执行上下文会变成正在运行的执行上下文。 然后,挂起的执行上下文可能会在以后的某个位置重新开始。
Any time one execution context is replaced by another like this, a new execution context is created and pushed onto the stack, becoming the current execution context.
每当一个执行上下文被这样的另一个替换时,就会创建一个新的执行上下文并将其压入堆栈,成为当前的执行上下文。
For a practical example of this concept in action in the browser, see the example below:
有关在浏览器中实际应用此概念的实际示例,请参见以下示例:
var x = 10;
function foo(a) {
var b = 20;
function bar(c) {
var d = 30;
return boop(x + a + b + c + d);
}
function boop(e) {
return e * -1;
}
return bar;
}
var moar = foo(5); // Closure
/*
The function below executes the function bar which was returned
when we executed the function foo in the line above. The function bar
invokes boop, at which point bar gets suspended and boop gets push
onto the top of the call stack (see the screenshot below)
*/
moar(15);
Then when boop returns, it gets popped off the stack and bar is resumed:
然后,当boop返回时,它将弹出堆栈,并恢复bar :
When we have a bunch of execution contexts running one after another — often being paused in the middle and then later resumed — we need some way to keep track of state so we can manage the order and execution of these contexts.
当我们有很多执行上下文接连运行时-通常在中间暂停,然后再恢复-我们需要某种方式来跟踪状态,以便我们可以管理这些上下文的顺序和执行。
And that is in fact the case. As per the ECMAScript spec, each execution context has various state components that are used to keep track of the progress the code in each context has made. These include:
事实确实如此。 根据ECMAScript规范,每个执行上下文都有各种状态组件,这些组件用于跟踪每个上下文中代码的进度。 这些包括:
Code evaluation state: Any state needed to perform, suspend, and resume evaluation of the code associated with this execution context
代码评估状态:执行,暂停和恢复与该执行上下文关联的代码的评估所需的任何状态
Function: The function object which the execution context is evaluating (or null if the context being evaluated is a script or module)
Function:执行上下文正在评估的函数对象(如果正在评估的上下文是脚本或模块,则为null)
Realm: A set of internal objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources
领域:一组内部对象,一个ECMAScript全局环境,在该全局环境范围内加载的所有ECMAScript代码以及其他相关状态和资源
Lexical Environment: Used to resolve identifier references made by code within this execution context.
词法环境:用于解析由代码在此执行上下文中进行的标识符引用。
Variable Environment: Lexical Environment whose EnvironmentRecord holds bindings created by VariableStatements within this execution context.
可变环境:词法环境,其EnvironmentRecord包含在此执行上下文中由VariableStatements创建的绑定。
If this sounds too confusing to you, don’t worry. Of all these variables, the Lexical Environment variable is the one that’s most interesting to us because it explicitly states that it resolves “identifier references” made by code within this execution context.
如果这听起来让您感到困惑,请放心。 在所有这些变量中,词法环境变量是我们最感兴趣的变量,因为它明确声明它可以解析代码在此执行上下文中进行的“标识符引用” 。
You can think of “identifiers” as variables. Since our original goal was to figure out how it’s possible for us to magically access variables even after a function (or “context”) has returned, Lexical Environment looks like something we should dig into!
您可以将“标识符”视为变量。 由于我们最初的目标是弄清楚即使在函数(或“上下文”)返回后,我们如何能够神奇地访问变量,因此词法环境看起来应该被我们研究!
Note: Technically, both Variable Environment and Lexical Environment are used to implement closures. But for simplicity’s sake, we’ll generalize it to an “Environment”. For a detailed explanation on the difference between Lexical and Variable Environment, see Dr. Alex Rauschmayer’s excellent article.
注意 :从技术上讲,可变环境和词法环境都用于实现闭包。 但是为了简单起见,我们将其概括为“环境”。 有关词法环境和可变环境之间差异的详细说明,请参见Alex Rauschmayer博士的精彩 文章 。
词汇环境 (Lexical Environment)
By definition:
根据定义:
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment. Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated. — ECMAScript-262/6.0
词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。 词法环境由环境记录和对外部词法环境的可能为空的引用组成。 通常,词法环境与ECMAScript代码的某些特定句法结构(例如,FunctionDeclaration,BlockStatement或TryStatement的Catch子句)相关联,并且每次评估此类代码时都会创建一个新的词法环境。 — ECMAScript-262 / 6.0
Let’s break this down.
让我们分解一下。
“Used to define the association of Identifiers”: The purpose of a Lexical Environment is to manage data (i.e. identifiers) within code. In other words, it gives meaning to identifiers. For instance, if we had a line of code “console.log(x / 10)”, it’s meaningless to have a variable (or “identifier”) x without something that provides meaning for that variable. The Lexical Environments provides this meaning (or “association”) via its Environment Record (see below).
“用于定义标识符的关联”: 词法环境的目的是在代码内管理数据(即标识符)。 换句话说,它使标识符有意义。 例如,如果我们有一行代码“ console.log(x / 10)”,那么拥有一个变量(或“标识符”) x却没有提供该变量含义的意义就毫无意义。 词法环境通过其环境记录(见下文)提供了这种含义(或“关联”)。
“Lexical Environment consists of an Environment Record”: An Environment Record is a fancy way to say that it keeps a record of all identifiers and their bindings that exist within a Lexical Environment. Every Lexical Environment has it’s own Environment Record.
“词法环境由环境记录组成”:环境记录是一种奇特的说法,它可以记录词法环境中存在的所有标识符及其绑定。 每个词法环境都有自己的环境记录。
“Lexical nesting structure”: This is the interesting part, which is basically saying that an inner environment references the outer environment that surrounds it, and that this outer environment can have its own outer environment as well. As a result, an environment can serve as the outer environment for more than one inner environment. The global environment is the only Lexical environment that does not have an outer environment. The language here is tricky, so let’s use a metaphor and think of lexical environments like layers of an onion: the global environment is the outermost layer of the onion; every subsequent layer below is nested within.
“词汇嵌套结构”: 这是有趣的部分,基本上是说内部环境引用了包围它的外部环境,并且该外部环境也可以有自己的外部环境。 结果,一个环境可以用作多个内部环境的外部环境。 全局环境是唯一没有外部环境的词法环境。 这里的语言很棘手,所以让我们用一个隐喻来思考像洋葱层这样的词汇环境:全局环境是洋葱的最外层; 下面的每个后续层都嵌套在其中。
Abstractly, the environment looks like this in pseudocode:
抽象地,环境看起来像这样的伪代码:
LexicalEnvironment = {
EnvironmentRecord: {
// Identifier bindings go here
},
// Reference to the outer environment
outer: < >
};
“A new Lexical Environment is created each time such code is evaluated”: Each time an enclosing outer function is called, a new lexical environment is created. This is important — we’ll come back to this point again at the end. (Side note: a function is not the only way to create a Lexical Environment. Others include a block statement or a catch clause. For simplicity’s sake, I’ll focus on environment created by functions throughout this post)
“每次评估这样的代码都会创建一个新的词法环境”:每次调用一个封闭的外部函数时,都会创建一个新的词法环境。 这很重要-我们将在最后再次回到这一点。 (附带说明:函数不是创建词法环境的唯一方法。其他函数包括block语句或catch子句。为简单起见,在本文中,我将重点介绍由函数创建的环境)
In short, every execution context has a Lexical Environment. This Lexical environments holds variables and their associated values, and also has a reference to its outer environment.
简而言之,每个执行上下文都有一个词法环境。 该词法环境包含变量及其关联的值,并且还引用了其外部环境。
The Lexical Environment can be the global environment, a module environment (which contains the bindings for the top level declarations of a Module), or a function environment (environment created due to the invocation of a function).
词法环境可以是全局环境,模块环境(包含模块顶级声明的绑定)或函数环境(由于调用函数而创建的环境)。
范围链 (Scope Chain)
Based on the above definition, we know that an environment has access to its parent’s environment, and its parent environment has access to its parent environment, and so on. This set of identifiers that each environment has access to is called “scope.” We can nest scopes into a hierarchical chain of environments known as the “scope chain”.
根据上面的定义,我们知道一个环境可以访问其父级环境,并且它的父级环境可以访问其父级环境,依此类推。 每个环境可以访问的这组标识符称为“范围”。 我们可以将范围嵌套到称为“作用域链”的层次结构环境链中 。
Let’s look at an example of this nesting structure:
让我们看一下此嵌套结构的示例:
var x = 10;
function foo() {
var y = 20; // free variable
function bar() {
var z = 15; // free variable
return x + y + z;
}
return bar;
}
As you can see, bar is nested within foo. To help you visualize the nesting, see the diagram below:
如您所见, bar嵌套在foo中 。 为了帮助您可视化嵌套,请参见下图:
We’ll revisit this example later in the post.
我们将在后面的文章中重新讨论该示例。
This scope chain, or chain of environments associated with a function, is saved to the function object at the time of its creation. In other words, it’s defined statically by location within the source code. (This is also known as “lexical scoping”.)
该作用域链或与功能关联的环境链在创建时保存到功能对象。 换句话说,它是根据源代码中的位置静态定义的。 (这也称为“词汇范围”。)
Let’s take a quick detour to understand the difference between “dynamic scope” and “static scope”, which will help clarify why static scope (or lexical scope) is necessary in order to have closures.
让我们快速绕道去了解“动态范围”和“静态范围”之间的区别,这将有助于阐明为什么要使用闭包需要静态范围(或词法范围)。
绕行:动态范围与静态范围 (Detour: Dynamic Scope vs. Static Scope)
Dynamic scoped languages have “stack-based implementations”, meaning that the local variables and arguments of functions are stored on a stack. Therefore, the runtime state of the program stack determines what variable you are referring to.
动态范围语言具有“基于堆栈的实现”,这意味着函数的局部变量和参数存储在堆栈中。 因此,程序堆栈的运行时状态确定您要引用的变量。
On the other hand, static scope is when the variables referenced in a context are recorded at the time of creation. In other words, the structure of the program source code determines what variables you are referring to.
另一方面,静态作用域是在创建时记录上下文中引用的变量的情况。 换句话说,程序源代码的结构决定了您要引用的变量。
At this point, you might be wondering how dynamic scope and static scope are different. Here’s two examples to help illustrate:
在这一点上,您可能想知道动态范围和静态范围有何不同。 这有两个例子可以帮助说明:
范例1: (Example 1:)
var x = 10;
function foo() {
var y = x + 5;
return y;
}
function bar() {
var x = 2;
return foo();
}
function main() {
foo(); // Static scope: 15; Dynamic scope: 15
bar(); // Static scope: 15; Dynamic scope: 7
return 0;
}
We see above that the static scope and dynamic scope return different values when the function bar is invoked.
上面我们看到,调用功能栏时,静态作用域和动态作用域返回不同的值。
With static scope, the return value of bar is based on the value of x at the time of foo’s creation. This is because of the static and lexical structure of the source code, which results in x being 10 and the result being 15.
在静态范围内, bar的返回值基于创建foo时的x值。 这是因为源代码的静态和词法结构,导致x为10,结果为15。
Dynamic scope, on the other hand, gives us a stack of variable definitions tracked at runtime — such that which x we use depends on what exactly is in scope and has been defined dynamically at runtime. Running the function bar pushes x = 2 onto the top of the stack, making foo return 7.
另一方面,动态作用域为我们提供了在运行时跟踪的变量定义堆栈-这样,我们使用的x取决于作用域中究竟是什么,并且在运行时已动态定义。 运行功能栏会将x = 2压入堆栈顶部,使foo返回7。
范例2: (Example 2:)
var myVar = 100;
function foo() {
console.log(myVar);
}
foo(); // Static scope: 100; Dynamic scope: 100
(function () {
var myVar = 50;
foo(); // Static scope: 100; Dynamic scope: 50
})();
// Higher-order function
(function (arg) {
var myVar = 1500;
arg(); // Static scope: 100; Dynamic scope: 1500
})(foo);
Similarly, in the dynamic scope example above the variable myVar is resolved using the value of myVar at the place where the function is called. Static scope, on the other hand, resolves myVar to the variable that was saved in the scope of the two IIFE functions at creation.
同样地,在变量myVar的上面的动态范围的例子是使用在函数被调用的地方myVar的值解决。 另一方面,静态作用域将myVar解析为在创建时保存在两个IIFE函数的作用域中的变量。
As you can see, dynamic scope often leads to some ambiguity. It’s not exactly made clear which scope the free variable will be resolved from.
如您所见,动态范围通常会导致一些歧义。 尚不清楚要从哪个范围解析免费变量。
关闭 (Closures)
Some of that may strike you as off-topic, but we’ve actually covered everything we need to know to understand closures:
其中一些可能会让您感到无聊,但实际上,我们已经介绍了了解闭包所需的所有知识:
Every function has an execution context, which comprises of an environment that gives meaning to the variables within that function and a reference to its parent’s environment. A reference to the parent’s environment makes all variables in the parent scope available for all inner functions, regardless of whether the inner function(s) are invoked outside or inside the scope in which they were created.
每个函数都有一个执行上下文,该上下文包含一个使该函数中的变量具有含义的环境以及对其父环境的引用。 对父级环境的引用使父级作用域中的所有变量可用于所有内部函数,而不管内部函数是在其创建范围的外部还是内部调用的。
So, it appears as if the function “remembers” this environment (or scope) because the function literally has a reference to the environment (and the variables defined in that environment)!
因此,似乎该函数似乎“记住”了该环境(或范围),因为该函数字面意义上是对该环境(以及该环境中定义的变量)的引用!
Coming back to the nested structure example:
回到嵌套结构示例:
var x = 10;
function foo() {
var y = 20; // free variable
function bar() {
var z = 15; // free variable
return x + y + z;
}
return bar;
}
var test = foo();
test(); // 45
Based on our understanding of how environments work, we can say that the environment definitions for the above example look something like this (note, this is purely pseudocode):
基于对环境工作原理的理解,可以说上述示例的环境定义如下所示(请注意,这纯粹是伪代码):
GlobalEnvironment = {
EnvironmentRecord: {
// built-in identifiers
Array: '<func>',
Object: '<func>',
// etc..
// custom identifiers
x: 10
},
outer: null
};
fooEnvironment = {
EnvironmentRecord: {
y: 20,
bar: '<func>'
}
outer: GlobalEnvironment
};
barEnvironment = {
EnvironmentRecord: {
z: 15
}
outer: fooEnvironment
};
When we invoke the function test, we get 45, which is the return value from invoking the function bar (because foo returned bar). bar has access to the free variable y even after the function foo has returned because bar has a reference to y through its outer environment, which is foo’s environment! bar also has access to the global variable x because foo’s environment has access to the global environment. This is called “scope-chain lookup.”
当调用函数test时 ,我们得到45,这是调用函数bar的返回值(因为foo return bar )。 即使函数foo返回后, bar仍可以访问自由变量y,因为bar通过其外部环境(即foo的环境)对y进行了引用! bar也可以访问全局变量x,因为foo的环境可以访问全局环境。 这称为“范围链查找”。
Returning to our discussion of dynamic scope vs static scope: for closures to be implemented, we can’t use dynamic scoping via a dynamic stack to store our variables.
回到我们对动态范围与静态范围的讨论:对于要实现的闭包,我们不能通过动态堆栈使用动态作用域来存储变量。
The reason is because it would mean that when a function returns, the variables would be popped off the stack and no longer available — which contradicts our initial definition of a closure.
原因是因为这意味着当函数返回时,变量将从堆栈中弹出并且不再可用,这与我们对闭包的初始定义相矛盾。
What happens instead is that the closure data of the parent context is saved in what’s known as the “heap,” which allows for the data to persist after the function call that made them returns (i.e. even after the execution context is popped off the execution call stack).
相反,发生的事情是将父上下文的关闭数据保存在所谓的“堆”中,这使数据在使它们返回的函数调用之后仍然存在(即,即使在执行上下文弹出执行后)调用堆栈)。
Make sense? Good! Now that we understand the internals on an abstract level, let’s look at a couple more examples:
合理? 好! 现在,我们从抽象的角度理解了内部原理,让我们看几个示例:
范例1: (Example 1:)
One canonical example/mistake is when there’s a for-loop and we try to associate the counter variable in the for-loop with some function in the for-loop:
一个典型的例子/错误是当有一个for循环时,我们尝试将for循环中的counter变量与for循环中的某些函数相关联:
var result = [];
for (var i = 0; i < 5; i++) {
result[i] = function () {
console.log(i);
};
}
result[0](); // 5, expected 0
result[1](); // 5, expected 1
result[2](); // 5, expected 2
result[3](); // 5, expected 3
result[4](); // 5, expected 4
Going back to what we just learned, it becomes super easy to spot the mistake here! Abstractly, here’s what the environment looks like this by the time the for-loop exits:
回到我们刚刚学到的东西,在这里发现错误变得非常容易! 抽象地,到for循环退出时,环境如下所示:
environment: {
EnvironmentRecord: {
result: [...],
i: 5
},
outer: null,
}
The incorrect assumption here was that the scope is different for all five functions within the result array. Instead, what’s actually happening is that the environment (or context/scope) is the same for all five functions within the result array. Therefore, every time the variable i is incremented, it updates scope — which is shared by all the functions. That’s why any of the 5 functions trying to access i returns 5 (i is equal to 5 when the for-loop exits).
这里的错误假设是结果数组中所有五个函数的作用域都不同。 相反,实际上发生的是结果数组中所有五个函数的环境(或上下文/作用域)都相同。 因此,变量i每次增加时,它都会更新作用域-所有功能都共享该作用域。 这就是尝试访问i的5个函数中的任何一个都会返回5的原因(当for循环退出时,i等于5)。
One way to fix this is to create an additional enclosing context for each function so that they each get their own execution context/scope:
解决此问题的一种方法是为每个函数创建一个附加的封闭上下文,以使它们各自获得自己的执行上下文/作用域:
var result = [];
for (var i = 0; i < 5; i++) {
result[i] = (function inner(x) {
// additional enclosing context
return function() {
console.log(x);
}
})(i);
}
result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4
Yay! That fixed it :)
好极了! 修复它:)
Another, rather clever approach is to use let instead of var, since let is block-scoped and so a new identifier binding is created for each iteration in the for-loop:
另一种相当聪明的方法是使用let而不是var ,因为let是块范围的,因此为for循环中的每次迭代创建一个新的标识符绑定:
var result = [];
for (let i = 0; i < 5; i++) {
result[i] = function () {
console.log(i);
};
}
result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4
Tada! :)
多田 :)
范例2: (Example 2:)
In this example, we’ll show how each call to a function creates a new separate closure:
在此示例中,我们将展示如何对函数的每次调用创建一个新的单独的闭包:
function iCantThinkOfAName(num, obj) {
// This array variable, along with the 2 parameters passed in,
// are 'captured' by the nested function 'doSomething'
var array = [1, 2, 3];
function doSomething(i) {
num += i;
array.push(num);
console.log('num: ' + num);
console.log('array: ' + array);
console.log('obj.value: ' + obj.value);
}
return doSomething;
}
var referenceObject = { value: 10 };
var foo = iCantThinkOfAName(2, referenceObject); // closure #1
var bar = iCantThinkOfAName(6, referenceObject); // closure #2
foo(2);
/*
num: 4
array: 1,2,3,4
obj.value: 10
*/
bar(2);
/*
num: 8
array: 1,2,3,8
obj.value: 10
*/
referenceObject.value++;
foo(4);
/*
num: 8
array: 1,2,3,4,8
obj.value: 11
*/
bar(4);
/*
num: 12
array: 1,2,3,8,12
obj.value: 11
*/
In this example, we can see that each call to the function iCantThinkOfAName creates a new closure, namely foo and bar. Subsequent invocations to either closure functions updates the closure variables within that closure itself, demonstrating that the variables in each closure continue to be usable by iCantThinkOfAName’s doSomething function long after iCantThinkOfAName returns.
在此示例中,我们可以看到对函数iCantThinkOfAName的每次调用都会创建一个新的闭包,即foo和bar 。 以后的调用要么关闭功能更新关闭自身内的闭包变量,这表明在每个封闭变量继续iCantThinkOfAName返回后,长须通过iCantThinkOfAName的DoSomething的功能使用。
范例3: (Example 3:)
function mysteriousCalculator(a, b) {
var mysteriousVariable = 3;
return {
add: function() {
var result = a + b + mysteriousVariable;
return toFixedTwoPlaces(result);
},
subtract: function() {
var result = a - b - mysteriousVariable;
return toFixedTwoPlaces(result);
}
}
}
function toFixedTwoPlaces(value) {
return value.toFixed(2);
}
var myCalculator = mysteriousCalculator(10.01, 2.01);
myCalculator.add() // 15.02
myCalculator.subtract() // 5.00
What we can observe is that mysteriousCalculator is in the global scope, and it returns two functions. Abstractly, the environments for the example above look like this:
我们可以看到, mysteryCalculator在全局范围内,它返回两个函数。 抽象地,以上示例的环境如下所示:
GlobalEnvironment = {
EnvironmentRecord: {
// built-in identifiers
Array: '<func>',
Object: '<func>',
// etc...
// custom identifiers
mysteriousCalculator: '<func>',
toFixedTwoPlaces: '<func>',
},
outer: null,
};
mysteriousCalculatorEnvironment = {
EnvironmentRecord: {
a: 10.01,
b: 2.01,
mysteriousVariable: 3,
}
outer: GlobalEnvironment,
};
addEnvironment = {
EnvironmentRecord: {
result: 15.02
}
outer: mysteriousCalculatorEnvironment,
};
subtractEnvironment = {
EnvironmentRecord: {
result: 5.00
}
outer: mysteriousCalculatorEnvironment,
};
Because our add and subtract functions have a reference to the mysteriousCalculator function environment, they’re able to make use of the variables in that environment to calculate the result.
由于我们的加法和减法函数引用了牢牢的计算器函数环境,因此它们能够利用该环境中的变量来计算结果。
范例4: (Example 4:)
One final example to demonstrate an important use of closures: to maintain a private reference to a variable in the outer scope.
最后一个示例说明了闭包的重要用法:在外部范围内维护对变量的私有引用。
function secretPassword() {
var password = 'xh38sk';
return {
guessPassword: function(guess) {
if (guess === password) {
return true;
} else {
return false;
}
}
}
}
var passwordGame = secretPassword();
passwordGame.guessPassword('heyisthisit?'); // false
passwordGame.guessPassword('xh38sk'); // true
This is a very powerful technique — it gives the closure function guessPassword exclusive access to the password variable, while making it impossible to access the password from the outside.
这是一项非常强大的技术,它使闭包函数guessPassword可以独占访问password变量,同时使它无法从外部访问密码 。
TL; DR (TL;DR)
Execution context is an abstract concept used by the ECMAScript specification to track the runtime evaluation of code. At any point in time, there can only be one execution context that is executing code.
执行上下文是ECMAScript规范用于以下目的的抽象概念: 跟踪代码的运行时评估。 在任何时间点,只有一个执行上下文正在执行代码。
- Every execution context has a Lexical Environment. This Lexical environments holds identifier bindings (i.e. variables and their associated values), and also has a reference to its outer environment. 每个执行上下文都有一个词法环境。 该词法环境拥有标识符绑定(即变量及其关联的值),并且还引用了其外部环境。
- The set of identifiers that each environment has access to is called “scope.” We can nest these scopes into a hierarchical chain of environments, known as the “scope chain”. 每个环境可以访问的标识符集称为“范围”。 我们可以将这些作用域嵌套到一个层次化的环境链中,称为“作用域链”。
- Every function has an execution context, which comprises of a Lexical Environment that gives meaning to the variables within that function and a reference to its parent’s environment. And so it appears as if the function “remembers” this environment (or scope) because the function literally has a reference to this environment. This is a closure. 每个函数都有一个执行上下文,该上下文包含一个词法环境(该词法环境赋予该函数内的变量含义)以及对其父级环境的引用。 因此,似乎该函数“记住”了该环境(或范围),因为该函数实际上是对该环境的引用。 这是一个关闭。
- A closure is created every time an enclosing outer function is called. In other words, the inner function does not need to return for a closure to be created. 每次调用封闭的外部函数时都会创建一个封闭。 换句话说,内部函数不需要返回就可以创建闭包。
- The scope of a closure in JavaScript is lexical, meaning it’s defined statically by its location within the source code. JavaScript中闭包的范围是词法的,这意味着它是由其在源代码中的位置静态定义的。
- Closures have many practical use cases. One important use case is to maintain a private reference to a variable in the outer scope. 闭包有许多实际用例。 一个重要的用例是在外部范围内维护对变量的私有引用。
结束语 (Clos(ure)ing remarks)
I hope this post was helpful and gave you a mental model for how closures are implemented in JavaScript. As you can see, understanding the nuts and bolts of how they work makes it much easier to spot closures — not to mention saving a lot of headache when it’s time to debug.
希望本文对您有所帮助,并为您提供了有关如何在JavaScript中实现闭包的思维模型。 如您所见,了解它们的工作原理可以使发现闭包变得容易得多,更不用说在进行调试时省去很多麻烦了。
PS: I’m human and make mistakes — so if you find any mistakes I’d love for you to let me know!
PS:我是人类,会犯错误-因此,如果您发现任何错误,我希望您能与我联系!
进一步阅读 (Further Reading)
For the sake of brevity I left out a few topics that might be interesting to some readers. Here are some links that I wanted to share:
为了简洁起见,我省略了一些读者可能感兴趣的主题。 这是我要分享的一些链接:
What’s the VariableEnvironment within an execution context? Dr. Axel Rauschmayer does a phenomenol job explaining it so I’ll leave you off with a link to his blog post: http://www.2ality.com/2011/04/ecmascript-5-spec-lexicalenvironment.html
执行上下文中的VariableEnvironment是什么? Axel Rauschmayer博士做了一个苯酚的解释工作,所以我将通过他的博客文章链接使您离开: http : //www.2ality.com/2011/04/ecmascript-5-spec-lexicalenvironment.html
What are the different types of Environment Records? Read the spec here: http://www.ecma-international.org/ecma-262/6.0/#sec-environment-records
有哪些不同类型的环境记录? 在此处阅读规格: http : //www.ecma-international.org/ecma-262/6.0/#sec-environment-records
Excellent article by MDN on closures: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
MDN关于闭包的出色文章: https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
- Others? Please suggest and I’ll add them! 其他? 请提出建议,我将添加它们!
翻译自: https://www.freecodecamp.org/news/lets-learn-javascript-closures-66feb44f6a44/
闭包的示例