Lexical Environment

The official definition of

The official ES2020 defines Lexical Environment as follows:

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.

A lexical environment is a specification type that defines the associations of identifiers with specific variables and functions based on the lexical nesting structure of ECMAScript code. A lexical environment consists of an environment record and an external lexical environment that may be null.

Say very detailed, but difficult to understand nan 🤔

Below, let’s go through a V8 JS compilation process to explain more intuitively.

V8 JS compilation process to more intuitive explanation

Roughly divided into three steps:

  • The first step is lexical analysisWhen V8 first gets the execution context, it Tokenizing/Lexing the code from top to bottom line, for examplevar a = 1;Will be divided intovara1;Such an atomic token. Lexical analysis = refers to the parameter of the registration variable declaration + function declaration + function declaration.
  • Step 2 syntax analysis: After the lexical analysis is complete, syntax analysis is performed. The engine parses the token into an abstract syntax tree (AST). In this step, the engine detects whether there is any syntax error
var a = 1;
console.log(a);
a = ;
// Uncaught SyntaxError: Unexpected token ;
// The code does not print out 1
Copy the code
  • Note: lexical analysis and grammatical analysis are not completely independent, but run interleaved. That is, it is not necessary to wait for all tokens to be generated before the parser processes them. As soon as a token is obtained, it is processed by the parser
  • Step 3 Code generation: The last step is to convert the AST into machine code that the computer can recognize

In the first step, we saw lexical analysis, which registers variable declarations, function declarations, and function declaration parameters so that subsequent code execution can know where to retrieve variable values and functions. This registry is the lexical environment.

The lexical environment consists of two parts:

  • Environment record: The actual location where variable and function declarations are stored, where variables are actually registered
  • A reference to an external environment: means that it has access to its external lexical environment, which is key to the scope chain being able to connect

The set of identifiers that each environment can access is called scope. We nested scopes layer by layer, creating a “scope chain.”

There are two types of lexical environments:

  • The global environment: is a lexical environment without an external environment, whose external environment is referenced asnull. Having a global object (window object) with its associated methods and properties (such as array methods) and any user-defined global variables,thisThe value points to the global object.
  • Function of the environment: Variables defined by the user in a function are stored inEnvironmental recordsContainsargumentsObject. A reference to an external environment can be a global environment or an external function environment that contains internal functions.

There are also two types of environmental records:

  • Declarative environment record: Stores variables, functions, and parameters. A functional environment contains declarative environment records.
  • Object environment record: Used to define the associations between variables and functions that occur in the context of global execution. The global environment contains object environment records.

If expressed in pseudocode, the lexical environment looks like this:

GlobalExectionContext = {  // Global execution context
  LexicalEnvironment: {    	  // lexical context
    EnvironmentRecord: {   		// Environment record
      Type: "Object".// Global environment
      // ...
      // The identifier is bound here
    },
    outer: <null>  	   		   // A reference to the external environment
  }  
}

FunctionExectionContext = { // Function execution context
  LexicalEnvironment: {  	  // lexical context
    EnvironmentRecord: {  		// Environment record
      Type: "Declarative".// Function environment
      // ...
      // The identifier is bound here // a reference to the external environment
	},
    outer: <Global or outer function environment reference>}}Copy the code

Such as:

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20.30);
Copy the code

Corresponding execution context, lexical environment:

GlobalExectionContext = {

  ThisBinding: <Global Object>, LexicalEnvironment: {EnvironmentRecord: {Type: "Object", // the identifier is binding here < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: C: undefined,} outer: <null>}} FunctionExectionContext = {ThisBinding: <Global Object>, LexicalEnvironment: {EnvironmentRecord: {Type: "Declarative", // 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: G: undefined, outer: <GlobalLexicalEnvironment>}}Copy the code

The lexical environment corresponds to the structure of our own code, that is, the lexical environment is the way our own code is written. The lexical environment is determined when the code is defined, regardless of where the code is called. So JS uses lexical scope (static scope), that is, it is statically scoped after the code is written.

Static scope vs dynamic scope

Dynamic scope is based on the stack structure, local variables and function parameters are stored in the stack, so the value of the variable is determined by the stack top execution context of the current stack when the code is running. Static scope means that the value of a variable is determined when it is created, and the location of the source code determines the value of the variable.

var x = 1;

function foo() {
  var y = x + 1;
  return y;
}

function bar() {
  var x = 2;
  return foo();
}

foo(); // Static scope: 2; Dynamic scope: 2
bar(); // Static scope: 2; Dynamic scope: 3
Copy the code

In this case, the execution structure of the static and dynamic scopes may be different. Bar is essentially executing foo. If it is statically scoped, the variable x in bar is determined when foo is created, which means that x is always 1 and the output should be 2 both times. Dynamic scopes, on the other hand, return different results depending on the x value at runtime.

Therefore, dynamic scopes often introduce uncertainty as to which scope a variable’s value comes from.

Most modern programming languages use static scoping rules, such as C/C++, C#, Python, Java, JavaScript, etc. Emacs Lisp, Common Lisp (both static scoping), Perl (both static scoping) use dynamic scoping. C/C++ macro used in the name of the dynamic scope.

Lexical environments and closures

A combination of a function bound with references to its surrounding state (lexical environment) (or the function enclosed by references) is a closure.

– the MDN

That is, closures are a combination of functions and the lexical environment in which they are declared

var x = 1;

function foo() {
  var y = 2; // Free variables
  function bar() {
    var z = 3; // Free variables
    return x + y + z;
  }
  return bar;
}

var test = foo();

test(); / / 6
Copy the code

Based on our understanding of the lexical environment, the above example can be abstracted into pseudocode as follows:

GlobalEnvironment = {
  EnvironmentRecord: { 
    // Built-in identifier
    Array: '<func>'.Object: '<func>'./ / and so on..

    // Customize the identifier
    x: 1
  },
  outer: null
};

fooEnvironment = {
  EnvironmentRecord: {
    y: 2.bar: '<func>'
  }
  outer: GlobalEnvironment
};

barEnvironment = {
  EnvironmentRecord: {
    z: 3
  }
  outer: fooEnvironment
};
Copy the code

As mentioned earlier, lexical scope is also called static scope, and variables are determined at lexical stage, that is, at definition time. Although called inside bar, because Foo is a closure function, it retains its scope even if it executes outside of its own lexical scope. A closure function is a function that encloses the context in which it is defined, forming a closure, so Foo doesn’t look for variables in bar, which is the nature of static scope.

To implement closures, we cannot store variables with a dynamically scoped dynamic stack. If so, when the function returns, the variable must go off the stack and no longer exist, which contradicts the original closure definition. In fact, the closure data of the external environment is stored in the “heap” so that the internal variable persists even after the function returns (even if its execution context is off the stack).

Three minutes a day