The concept of scope

One of the most basic features of modern programming languages is the ability to store values in variables for later use in modification. It is this function that brings state to the program.

In JavaScript, a scope is a well-designed set of rules for storing variables.

Brief introduction to compilation principles

JavaScript is usually categorized as a “dynamic” or “interpreted execution” language, but it’s really a compiled language. Unlike traditional compiled languages, it is not pre-compiled and the compiled results cannot be portable across distributed systems.

For example, in V8 engines, in order to improve the performance of JavaScript code, it first compiles it into native machine code before running, and then executes the machine code to increase the speed.

  • Word segmentation/lexical analysis

    This process breaks up the code composed of characters into blocks of code that make sense to the program, called lexical units.

    For example, var foo = ‘bar’ is usually broken down into these lexical units: var, foo, =, ‘bar’

  • Parsing/parsing

    This process converts lexical units into a “tree of progressively nested elements representing the syntax of the program,” known as an “Abstract syntax tree” (AST).

  • Code generation

    Convert the above abstract syntax tree into machine-executable code

The JavaScript engine is much more complex than a compiler for a three-step language. For example, there are specific steps to optimize performance during parsing and code generation, including optimizing for redundant elements.

For JavaScript, most of the time compilation occurs in the first few microseconds of code execution, and any snippet of code is compiled before execution. So the JavaScript compiler first compiles var foo = ‘bar’, then gets ready to execute it, and usually executes it right away.

Coordination of engine, compiler, and scope in assignment operations

  • Engine: responsible for compiling and executing JavaScript programs from start to finish
  • Compiler: responsible for parsing and code generation
  • Scope: Responsible for collecting and maintaining a set of queries consisting of all variables

Var foo = ‘bar’ is probably a simple statement. In fact, JavaScript execution splits it into two completely different declarations.

  1. The compiler first decompresses this code into lexical units and then parses it into a tree structure. (This code will be handled differently than expected during the next code generation.)
  2. encountervar foo, the compiler checks to see if a variable with the same name already exists in the scope. If so, the compiler ignores the declaration and continues compiling. Otherwise it generates code that declares a new variable in the current scope’s variable collection, namedfoo
  3. The compiler then generates the runtime code for the engine to processfoo = 'bar'The assignment operation.
  4. The engine will first query whether the current scope has a namefooThe variables. This variable is used if there is an engine, otherwise it is searched all the way to the upper scope.
  5. And if we find itfooThis variable, it’s going to'bar'Assign to it, otherwise throw an exception.

Summary: Assignment to a variable performs two actions: first, the compiler declares the variable in the current scope (if it’s not already declared); The runtime engine then looks for the variable in scope and assigns it if it can be found.

LHS query vs RHS query

When the engine executes the code generated by the compiler, it looks for foo to see if it has been declared. The search process is assisted by scope. In our example, the engine is doing an LHS query for the variable foo, and there is another lookup type called an RHS query. As the name suggests, they mean Left hand side and Right hand side

  • LHS: Variable appears on the left side of an assignment (find who the assignment is targeting)
  • RHS: Variable appears elsewhere (look for the source of the value)
// Consider the code below
console.log(foo)
Copy the code

In this case, the reference to foo is the RHS query, where foo is not given any value. Instead, we need to find the value of foo to pass to the log method.

// By contrast
foo = 'bar'
Copy the code

In this case, the query for foo is an LHS query. We don’t care what the current value of foo is, we just want to find the target for the assignment.

// Look at the code below
function foo(a) {
  console.log(a)
}

foo('bar')
Copy the code

This code has both LHS queries and RHS queries

  1. The last linefoo(...)A call to a function requires a pair offooRHS query → FoundfooThe value of the
  2. There is an implicita = 'bar', you need toaPerform LHS query
  3. console.log(a)aPerform an RHS query
  4. console.log(...)It also needs to be rightconsoleObject for RHS query

Nesting of scopes

As we said at the beginning of this article, a scope is a set of rules for finding variables by name. In practice, several scopes need to be considered simultaneously.

Nesting of scopes occurs when a block or function is nested within another block or function. Therefore, if the target variable is not found in the current scope, it will be searched up to the global scope.

// Consider the following code
function foo(a) {
  console.log(a + b)
}

var b = 258;

foo(369)
Copy the code

The RHS query against B cannot be done inside Foo, but can be done in the scope above (global scope in this case).

LHS and RHS queries are searched layer by layer in the scope until they are found (or reach the global scope).

ReferenceError

The last section mentioned LHS. RHS always looks for variables in the scope layer by layer, but what if we reach the global scope and still no variables are found?

This is where it makes sense to distinguish BETWEEN LHS and RHS queries.

If the RHS query does not find the required variable in any of the nested scopes, the engine throws a ReferenceError.

If the LHS query does not find the required variable in any of the nested scopes, the engine creates a variable with that name in the global scope and returns it to the engine.

Note: Strict mode is introduced in ES5. One of the differences between strict mode and normal mode is that global variables are automatically or implicitly created in bases. Therefore, when an LHS query fails in strict mode, no global variables are created and returned, and the engine also throws a ReferenceError.

conclusion

  • Scope is a set of rules that determine where and how to find variables. If the purpose of the lookup is to assign a value to a variable, the LHS query is used; If the goal is to get the value of the variable, the RHS query is used.
  • The JavaScript engine compiles the code before it executes. In the process, likevar foo = 'bar'This declaration is broken down into two separate steps.
    1. var fooDeclare a new variable in its scope. This is done before the code executes.
    2. The followingfoo = 'bar'The (LHS) variable is queriedfooAnd assign a value to it.
  • Both LHS and RHS queries start in the current execution scope, and if necessary (no variables are found in the current scope) they continue to search for the target variable in the upper scope until they reach the global scope, stopping whether they are found or not.
  • An unsuccessful RHS lookup results in a throwReferenceError, an unsuccessful LHS lookup causes a global variable to be automatically and implicitly created (in non-strict mode) or thrownReferenceError(In strict mode).