Lexical Environment — The hidden part to understand Closures
Closures can be a daunting concept for players new to JavaScript. You can find a lot of definitions of closures on the Internet, but I think most of them are vague and don’t explain why closures exist at all.
Today we will demysmal these concepts according to the ECMAScript 262 specification, including execution context, lexical environment, and identifier resolution. It is worth mentioning that, because of these mechanisms, we will learn that all functions in ECMAScript are closures.
I’ll first explain the terms mentioned above, then show you a code example explaining how all these pieces work together, which will help solidify your understanding.
Execution context
The JavaScript interpreter creates a new context when we are about to execute a function or script we have written. Each script/code begins in a place called the Global Execution Context. Each time a function is called, a new execution context is created and placed at the top of the execution stack. Nested calls to another function within a function will follow the same pattern:
Take a look at what happens when the code is executed:
- A global execution Context is created and placed at the bottom of the execution stack.
- call
bar
A new bar Execution Context is created and placed at the top of the global Execution Context. - when
bar
Call nested functionsfoo
A new Foo Execution Context is created and placed at the top of the BAR Execution Context. - when
foo
When it returns at the end of execution, its execution context pops out of the execution stack and is returned tobar
In the execution context of. bar
After execution, it returns to the global execution context until finally the entire stack is cleared.
The execution stack runs in a first-in-last-out (LIFO) fashion, with the execution context at the bottom waiting for the execution context at the top to finish before continuing.
Conceptually, the data structure for the execution context looks like this:
// Execution context in ES5
ExecutionContext = {
ThisBinding: <this value>, VariableEnvironment: { ... }, LexicalEnvironment: { ... }}Copy the code
If the above data structures are confusing, don’t worry, we’ll cover them soon. The key point to keep in mind is that each invocation of an Execution context has two phases: the Creation Stage and the Execution Stage. The creation phase is when the execution context has been created but not yet invoked.
What happens during the Creation Stage:
VariableEnvironment
Components are used to store variable, parameter, and function declarations. useundefined
To initialize thevar
Declared variables.- determine
this
Pointing to. LexicalEnvironment
Just at this stageVariableEnvironment
A copy of.
At the Execution Stage:
- Variable assignment.
LexicalEnvironment
Used to resolve bindings (identifiers).
At this point, let’s begin to understand what lexical environments are.
Lexical Environment
As stated in ECMAScript specification 262 (8.1) :
A lexical environment is a specification for defining the associations of identifiers, specific variables, and functions in the lexical nesting structure of ECMAScript code.
To simplify, lexical environment mainly consists of two parts: environment record and reference to the outer lexical environment.
var x = 10;
function foo() {
var y = 20;
console.log(x + y); / / 30
}
// In theory, the lexical environment consists of two main parts:
// Environment record, a reference to the external lexical environment
// Global lexical environment
globalEnvironment = {
environmentRecord: {
x: 10
},
outer: null // The global lexical environment has no external references, so the arrow points to NULL
}
// The lexical context of the "foo" function
fooEnvironment = {
environmentRecord: {
y: 20
},
outer: globalEnvironment
}
Copy the code
It looks something like this:
As you can see, when the program starts parsing the identifier “x” in the execution context of Foo, it will be retrieved through the external lexical environment reference. This process is called identifier resolution, and identifier resolution occurs at run time in the execution context.
To sum up, looking back at the structure of the execution context based on an understanding of the lexical environment, see what happens:
- VariableEnvironment components (VariableEnvironment): its
environmentRecord
The initial storage of variables, parameters, and functions that are assigned values after they enter the Execution Stage.
function foo(a) {
var b = 20;
}
foo(10);
// VariableEnvironment during the Creation Stage of foo's execution context
fooContext.VariableEnvironment = {
environmentRecord: {
arguments: { 0: 10.length: 1.callee: foo},
a: 10.b: undefined
},
outer: globalEnvironment
};
// After the Execution Stage of the foo function Execution context,
// Variable values in environmentRecord are assigned
fooContext.VariableEnvironment = {
environmentRecord: {
arguments: { 0: 10.length: 1.callee: foo },
a: 10.b: 20
},
outer: globalEnvironment
};
Copy the code
- LexicalEnvironment component: in the creation phase, the LexicalEnvironment is just a copy of the mutable environment, running in the execution context to determine the binding (pointing to variables) of the identifier in the execution context.
The variable environment component (VE) and lexical environment component (LE) are essentially the same in that they both statically capture references from the current execution context to the Outer lexical environment during the Creation Stage, which is why closures exist.
A statically captured reference to the external lexical environment by the current function execution context results in closure formation.
Identifier resolution – scope-chain lookup
Before we understand closures, let’s look at how scope chains are created in the execution context. As mentioned earlier, each execution context has a lexical environment for parsing identifiers. All local bindings of the execution context are stored in the environment declaration record of the current lexical environment, and if the lexical environment does not find an identifier, the resolution is looked up from the external (parent) environment record until the identifier is successfully resolved. If the corresponding identifier is not found successfully, a ReferenceError error is thrown.
This mechanism is very similar to prototype chains. The key is that at the Creation Stage of the Execution context, the lexical environment (statically) captures a reference to the Outer lexical environment and uses it at the Execution Stage.
closure
As mentioned above, references to the external lexical environment are statically stored in the lexical environment of the current execution context during the function creation phase, whether or not the function is used later. Consider the following example:
Example 1:
var a = 10;
function foo() {
console.log(a);
}
function bar() {
var a = 20;
foo();
}
bar(); // Will print "10"
Copy the code
The function foo captures the binding of “a”, 10, during the creation phase. So when the function foo is called, the parse value of the “a” identifier is 10, not 20.
Conceptually, the identifier resolution process is as follows:
// Find identifier "a" in the environment record of "foo"
-- foo.[[LexicalEnvironment]].[[Record]] --> "not found"
// If not found (" not found "), look in Foo's "references to Outer lexical environment (Outer)"
--- global[[LexicalEnvironment]][[Record]] --> "found 10"
// Parsing returns the value of the identifier: 10
Copy the code
Example 2:
function outer() {
let id = 1;
return function inner() {
console.log(id); }}const innerFunc = outer();
innerFunc(); / / output 1
Copy the code
When an external function returns from execution, its execution context is popped from the execution stack. But when we call innerFunc() later, the lexical environment of the inner function statically captures the id identifier of the outer lexical environment, so it still prints the desired value correctly.
// Find the identifier "id" in the environment record of "inner"
-- inner.[[LexicalEnvironment]].[[Record]] --> "not found"
// If not found (" not found "), look in inner's "references to the Outer lexical environment"
-- outer[[LexicalEnvironment]][[Record]] --> "found 1"
// Parsing returns the value of the identifier: 1
Copy the code
conclusion
- The execution stack follows the first in, last out (LIFO) data structure.
- Our code/script runs in a global execution context.
- Calling a function creates a new execution context (a), and if it has a nested function call, a new execution context (b) is created at the top of its context (a). When a function completes execution, its execution context is popped from the execution stack and returned to the execution context below it.
- Lexical Environment mainly consists of two parts: Environment Record and reference to outer Environment (generally described by outer).
- Both the VariableEnvironment and the LexicalEnvironment statically capture references to the external LexicalEnvironment in the execution context.
- All functions statically capture external references to their execution environment during the Creation Stage. Even after the external function has finished executing and the stack has been popped, the nested internal function can still access the external lexical environment through the lexical environment. Such a mechanism is the rationale behind JavaScript closures.