A preface.

For a programming language, variable storage and access, is one of the most basic functions. Today, will introduce you in depth, JS procedures in the run, how to find variables, which requires a good set of norms to regulate, this set of rules, is the scope

Two. V8 JS code running steps

Before we introduce scope, we need to introduce the flow of V8 JS code. There are many flow charts of V8 operation on the Internet, as follows:

The latter steps, such as Ignition’s bytecode and TurboFan’s optimized compiler, are mostly V8’s efforts to optimize performance and won’t be covered in this article. This article focuses on the first few steps. As you can see, V8 will first parse and generate the AST abstract syntax tree after receiving the JS source code, and then go through several compilation steps to generate the machine code and run it. The scope rules are determined when they are parsed into the AST step

The following uses the HELlo-world program in V8 source code as an example to explain the process:

github.com/v8/v8/samples/hello-world.cc

Before introducing the code flow shown above, a few concepts are introduced:

Isolate:

  • An Isolate is an independent VIRTUAL machine. Corresponding to one or more threads. But only one thread can enter at a time.
  • All the isolates are completely isolated from each other and cannot share any resources.
  • If the Isolate is not explicitly created, a default Isolate is automatically created

HandleScope:

  • Represents the scope of the JS object’s life cycle.
  • In V8, memory allocation is done in the V8 Heap, and JavaScript values and objects are placed in the V8 Heap. The Handle is a reference to an object in the Heap. V8 To manage memory allocation, GC needs to keep track of all objects in V8. HandleScope is used to manage Handle
  • Handle can be divided into Local and Persistent. Local is Local, creating a Local reference to the JS object, which is also managed by HandleScope. Persistent creates a persistent reference to a JS variable, similar to global, that is not affected by HandleScope and can be extended to different functions.

Context:

  • Can be interpreted as “execution context” or “execution environment”
  • Whenever the execution flow of a program enters an executable code, it enters an execution environment

The schematic diagram of the relationship among the three is as follows:

Going back to the original source code screenshot above, the following steps are probably done:

  1. Defines an ISOLATE
  2. Under ISOLATE, a handle_scope is defined. The life cycle of handle_SOPE determines the validity of all v8::Local declaration cycles
  3. Defines a context and switches into it
  4. Compile JS source code into bytecode
  5. In the current context, the bytecode compiled in the previous step is run

Scope is related to the execution context

In the second section above, you briefly walked through the V8 process of executing JS code. To summarize, JavaScript is an interpreted language, and the execution of JavaScript is divided into two stages: “explain” and “execute”, as follows:

Interpretation stage:

  • Lexical analysis
  • Syntax analysis
  • Scope rules are determined

Execution stage:

  • Create the execution context context
  • Execute function code
  • The garbage collection
  1. The difference between

Many students tend to confuse the two concepts of “scope” and “execution context”. Indeed, there is a certain correlation between the two concepts, but there are differences:

  • A scope can be abstracted as a set of rules for finding variables by name that govern how the JS engine looks up variables by identifier names. A series of nested scopes form a scope chain (defined in JavaScript that you don’t know)

  • The execution context, described in the previous section, is the function execution environment that V8 creates before the function runs

  • The scope is determined during AST parsing and does not change; The execution context, which is determined at execution time, can change. Here’s an example:

    var a = 10; function fn() { var b = 20; function bar() { console.log(this.b); // 200 console.log(a + b); // 30 } return bar; } var x = fn(), b = 200; x();

The result printed above is 200, 30, because for this.b, this refers to the execution context determined; When AST parses the definition of bar function, the scope chain of bar function has been defined as bar -> fn -> global. Therefore, variable B will look along the scope chain and find the definition in fn with a value of 20

  • A scope may contain several contexts. This is because every time a function scope calls another function, the execution context needed to call the function is created. However, the number of times a function is called is variable and needs to be determined at run time
  1. This section describes the function execution process

In order to further introduce the principle of scope and execution context, we take the execution of a function as an example to describe in detail, and introduce some of the concepts:

var scope = "global scope";
function checkscope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f();
}
checkscope();
Copy the code
  1. Executes global code, creating a global execution context that is pushed into the execution context stack

    ECStack = [ globalContext ];

Execution context stack: As described above, V8’s execution flow creates an execution context for executable code before it enters it. The execution environment that the execution stream enters in turn logically forms a stack, which is called the execution context stack. The bottom of the stack is always the global environment, and the top of the stack is the active current execution environment (the browser is always executing, the context at the top of the stack)

  1. Global context initialization (initialize the VO variable object of the global environment, determine the Scope of the global environment, bind this of the global environment)

    globalContext = { VO: { global: window, scope: undefined, checkscope: reference to function checkscope }, Scope: [globalContext.VO], this: globalContext.VO }

Variable object VO:

  • Stores variable and function declarations defined in the context; It’s just like a normal object except we can’t access it, right

  • For functions, the initialization phase before execution is called a variable object, which becomes an active object during execution

  • Each execution environment has a variable object associated with it, which stores variables, functions, and formal parameters declared in the context

  1. The checkScope function performs the pre-execution phase. At the same time of initialization, the checkScope function is created, saving the global environment’s scope chain into the function checkScope’s internal property [[scope]]

    checkscope.[[scope]] = [ globalContext.VO ];

    globalContext = { VO: { global: window, scope: “global scope”, checkscope: reference to function checkscope }, Scope: [globalContext.VO], this: globalContext.VO }

  2. Execute the checkScope function, create the checkScope function execution context, checkScope function execution context, is pushed into the execution context stack

    ECStack = [ checkscopeContext, globalContext ];

  3. Initialize the checkScope function execution context. There will be the following steps:

    1. Use Arguments to create the active object checkScopecontext.ao
    2. Using checkScopecontext. AO and checkScope.[[scope]], form the scope chain of checkScopecontext. scope
    3. Bind this to undefined (binding to global objects in non-strict mode)

    checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined }

Active object AO:

  • Properties in a variable object cannot be accessed until the current environment is executed. However, after the execution phase, the variable object is transformed into an active object, so the active object and the variable object are really the same thing, but in a different life cycle of the execution environment

  • AO actually contains VO. As well as VO, AO also contains parameters for a function, and arguments as a special object

  1. F phase before function execution. Update f [[scope]], checkscopeContext. AO. The scope of the assignment

    f.[[scope]] = [ checkscopeContext.AO, globalContext.VO ];

    checkscopeContext = { AO: { arguments: { length: 0 }, scope: “local scope”, f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined }

  2. Execute f function, create f function execution context, f function execution context is pushed into the execution context stack

    ECStack = [ fContext, checkscopeContext, globalContext ];

  3. F function execution environment initialization (refer to step E)

    fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined }

  4. F function code execution. An RHS lookup is required on the scope. Find starts looking up the scope chain from the currently active object in the scope chain

/ / lookup process: 1. FContext. AO. Scope without the variable declarations, continue to 2. CheckscopeContext. AO. The scope is the variable declarations, get its value for the "local scope"Copy the code

LHS and RHS: indicates the two query methods of the V8 engine

  • LHS: When a variable appears in the code, the purpose is to store it. That is, what we care about is to find the container of the variable itself to store and assign different data, rather than what is stored in the container now

  • RHS: The purpose is just to use this variable, that is, only care about the contents of the variable, do not need to care about the variable in which container

  1. F function completed, return “local scope”. The f function context pops up from the execution context stack

    ECStack = [ checkscopeContext, globalContext ];

  2. At the end of f, the checkScope function retrieves the return value of f execution “local scope”, and continues

  3. CheckScope returns the value “local scope”. The checkScope function context, popped from the execution context stack

    ECStack = [ globalContext ];

  4. The code flow goes back to the checoScope call in the global execution environment, gets the checkScope return value, and continues execution

  5. Until the program terminates, or the page closes. The global context is removed from the stack and destroyed

Author: Lu Hantao