Closure in JS

Before we talk about closures in JS, we need to introduce a few concepts to our friends.

1. Execution context

2. Execute the context stack

3. Lexical environment

(We assume that you are already familiar with the three basic points, but we will add some points later to make sure that you can quickly get familiar with the three basic points, but don’t explain them in too much detail.)

Execution context

There are four common executable code environments that can be execution contexts:

1. Global environment

2. Block-level environment

3. Functional environment

4. Eval environments (outside the scope of this discussion)

Execution context stack

Javascript is single-threaded, meaning that only one event is executed at a time. The other execution context associated with it. Is stored in a special data structure called the execution context stack. When code executes, it enters the global context by default, and if a function is called in the global context, a function execution context is created and placed at the top of the execution context stack.

Lexical environment

In JavaScript, each running function, block of code, and script as a whole has an internal (hidden) correlation object called the Lexical Environment.

Lexical environment object composition

  • Environment Record: An object that takes all local variables as its properties (including some additional information, such as variables, functions, parameters).
  • References to outer lexical environment: Lexical environment that is usually nested for code other than the current code.

All functions are “born” remembering the lexical context in which they were created. Technically, there’s no magic here: all functions have a hidden attribute named [[Environment]], which holds a reference to the lexical Environment in which the function was created.

Now that the basic knowledge is introduced, let’s start with the implementation of context on and off the stack

Code execution process

The Execution Context has three components:

  • LexicalEnvironment: is a LexicalEnvironment.
  • VariableEnvironment: Is also a LexicalEnvironment, which generally refers to the same LexicalEnvironment as LexicalEnvironment.
  • ThisBinding: This is the usual this in code.

The JS engine executes code as executable code, each time in the following steps:

  • 1: Create a new Execution Context
  • 2: Creating a new Lexical Environment (Lexical Environment)
  • 3: point LexicalEnvironment and VariableEnvironment to the newly created LexicalEnvironment
  • 4: Pushes the execution context onto the execution stack and becomes the running execution context
  • 5: Execute code
  • 6: After execution, pop the execution context out of the execution stack

To explain to my friends why there are two lexical contexts in an execution context: The VariableEnvironment component is used to register var function variable declarations, and the LexicalEnvironment component is used to register variable declarations such as let const class.

Before ES6, there was no block-level scope. After ES6, we can use let const to declare block-level scope. The two lexical environments are designed to implement block-level scope without having to affect var variable declarations and function declarations.

  • 1: first, in a running execution context, the LexicalEnvironment consists of LexicalEnvironment and VariableEnvironment, which are used to register all variable declarations.
  • 2: When executing block-level code, LexicalEnvironment first records it as oldEnv.
  • 3: Create a new LexicalEnvironment (outer pointing to oldEnv), record it as newEnv, and set newEnv to the LexicalEnvironment of the executing context.
  • 4: within block-level codelet constWill be registered in newEnv, butvarDeclarations and function declarations are still registered in the original VariableEnvironment.
  • 5: After block level code execution, restore oldEnv to the LexicalEnvironment of the executing context.

Normally, after the execution context at the top of the execution context stack has finished executing, it will be removed from the execution stack and its lexical environment link will be lost. We know that each function creates a new execution context as it executes, and also creates its own lexical Environment. Each function’s lexical Environment has a [[Environment]] that holds (points to) the lexical Environment above it. [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] : [[Environment]] That is, variables that can be retrieved from the execution context in which they are being pushed. This is a closure in JS syntax

V8 optimizations for closures

Closure in JS syntax refers to a function whose execution context is created in [[Environment]] lexical Environment. When the execution context is removed from the stack, the lexical Environment object of the sub-function is not recycled because [[Environment]] refers to the lexical Environment, so the variables can be accessed. But V8 has made some improvements to this mechanic, see below

Let’s take a look at what variables are stored in the [[Scopes]]

It packs only A, B and the global environment in the [[Scopes]] (the [[Scopes]] of each function packs the global environment).

It is obvious that functions have a [[Scopes]] hidden attribute, which uses V8’s own lazy and pre-parser mechanisms to create functions that know which variables are being used in the function and then package them in [[Scopes]]. In short, only external references are packaged on the property.

When out3 is called, the JS engine takes the packaged Closure stack in [[Scopes]] (the value of [[Scopes]] is a stack structure) and sets it to a new scope chain, which is equivalent to a subset of the old one.

When the top execution context in the execution context stack goes out of the stack, v8’s mechanism reduces the stress on memory by simply packing the required variables into [[Scopes]], and the pushed top execution context is destroyed. (I’m not sure if the Scopes are actually destroyed, but the variables used in the execution context are already packaged into the [[Scopes]] property of the corresponding function. If they are not destroyed, they will not exist at all.)