What is scope?

Scope is a word you hear and see often in the programming world, and almost every programmer has been asked about it at some point. On the front end, you’re interviewing for JavaScript knowledge, which is a very basic question. But early years I fell into a long-term “can only be understood can not be explained”, I do not know whether there are many friends and I have the same experience, so I took the time to read the things sorted out. Share the extracted stuff with everyone, please correct any inaccuracies. Probably the most common interpretation of scope is this:

Scope is the scope of a variable (identifier) and controls its visibility.

But what exactly is it? Is it a region? Or is it a rule?

I remember the definitive JavaScript Guide describing variable scope in this way:

The scope of a variable is the area in the program source code that defines the variable. Global variables have global scope and are defined anywhere in the JavaScript code. Variables declared inside a function, however, are defined only inside the function. They are local variables whose scope is local. Function parameters are also local variables; they are only defined within the body of the function.

This description gives the reader a general idea of what scope is, which can be understood here as a “region”.

When I first saw this phrase two years ago, I still couldn’t figure out what scope was, even though I had an outline in mind. I thought I’d dig a little deeper into what scope is.

What is compilation?

To figure out what scope is, we need to know a little bit about how JavaScript is compiled. From my first experience with JavaScript, everything I knew told me that JavaScript was a “dynamic” or “interpreted execution” language, but I later learned that it was actually a compiled language. Are you surprised? Unlike traditional compiled languages, JavaScript is not compiled ahead of time and the compiled results cannot be ported across distributed systems.

So what is the compilation process? It can be divided into three steps:

  1. Word segmentation/lexical analysis(Tokenizing/Lexing) This process breaks a string of characters into blocks of code that make sense (for the programming language), called lexical units (tokens). For example, consider programsvar a = 2;. The program is usually broken down into the following lexical units: var, a, =, 2; . Whether Spaces are considered lexical units depends on whether they make sense in the language.
  2. Parsing/parsingThis process is to convert a stream of lexical units (arrays) into a tree of nested elements that represents the syntaxary structure of the program. This Tree is called the Abstract Syntax Tree (AST can be found in various frameworks and Babel).var a = 2;The abstract syntax tree may have a top-level node called VariableDeclaration, followed by a child called Identifier (which has a value of A), and a child called AssignmentExpression. The AssignmentExpression node has a child called NumericLiteral, which has a value of 2.
  3. Code generationThe process of turning an AST into executable code is called code generation. This process depends on language, target platform, and so on. Regardless of the details, the short answer is that there is a wayvar a = 2;The AST is converted to a set of machine instructions that create a variable called A (including allocating memory, etc.) and store a value in A.

Of course, the JavaScript engine is much more complex than the three-step compiled language. But JavaScript compilation mostly happens a few microseconds or less before the code executes. Behind the scope we will discuss, the JavaScript engine uses various methods (such as JIT, which can delay compilation or even perform recompilation) to ensure optimal performance.

Understanding scope

We’ll use the word scope a zillion times here, so you can read it exactly as you read it. This does not affect our final understanding of scope.

Or var a = 2 this line of code, through the above we can know what is compile part, the compiler will first break this code into the lexical units, then the lexical unit solution form a tree structure (AST), but when the compiler to code generation, its approach to this code and expectations are different.

When we see this line of code, summarized in pseudocode to someone else, we might say something like: “Allocate memory for a variable, name it a, and store the value 2 in this variable (memory).” However, this is not entirely true.

The compiler actually does something like this:

  1. encountervar a, the compiler asks the scope if there is already a variable of that name in the collection of the same scope. If so, the compiler ignores the declaration and continues compiling. Otherwise it will require the scope to declare a new variable in the collection of the current scope and name it a.
  2. The compiler then generates the code for the engine to run, which is used for processinga = 2This assignment operation. When the engine runs, it first asks the scope if there is a variable called A in the current set of scopes. If so, the engine uses this variable. If not, the engine continues to look for the variable.

The compiler declares variables in scope (if not). 2. The engine looks for the variable when running the code and assigns it if it exists;

In step 2 above, the engine looks for variable A to determine if it has already been declared when it executes code needed for runtime. The lookup process is assisted by the scope, but how the engine performs the lookup affects the final result.

Var a = 2; In this example, the engine will perform an LHS query for variable A. There is, of course, an RHS query. So what are LHS and RHS queries? Here L stands for left, R stands for right. A common and loosely interpreted interpretation of LHS and RHS is that LHS queries are performed when variables appear on the left side of an assignment operation and RHS queries are performed when variables appear on the right.

A more accurate description, then, is that an RHS query is nothing more than simply looking up the value of a variable, whereas an LHS query is trying to find the container of the variable itself so that it can be assigned a value. In this sense, RHS is not really “the right side of assignment”, but rather “not the left side”. So, we can read RHS as the Retrieve his source value, which means, “Get the value of something.”

So let’s take a look at some code to understand LHS and RHS in more depth.

function foo(a) {
  console.log(a)
}

foo(2)
Copy the code

From this code, let’s first look at console.log(a).

Where the reference to A is an RHS reference, because we are fetching the value of a. And pass this value to console.log(…). Methods.

By contrast, for example: a =2 // foo(2) is implicitly assigned, and the reference to a is an LHS reference, because we don’t really care what the current value is, as long as we want to find a target for the =2 assignment.

Of course, the above program has more than one LHS and RHS reference:

function foo(a) {
  // there is an implicit LHS reference to the parameter a.
  
  // RHS reference is made to the log() method, asking if the log() method exists on the console object.
  // RHS reference to a in log(a).
  console.log(a)
}

// To call the foo() method here, you need to call the RHS reference to foo. It means "Go find the value foo and give it to me."
foo(2)
Copy the code

Note that we often declare function foo(a) {… Var foo = function(a) {… }, this function is an LHS query. There is a slight difference, however, that the compiler can handle declaration and value definition at the same time as code generation. For example, when the engine executes the code, there is no thread dedicated to “assigning” a function value to Foo. Therefore, it is not appropriate to understand function declarations in the form of LHS queries and assignments discussed earlier.

Does this give you an understanding of how scopes work? But what it is, it’s still a little vague, I don’t know how to express it. Anyway, let’s look at what a scope chain is.

The scope chain

Let’s look at a code example:

functionfoo(a) { console.log(a + b) } var b = 2; foo(2); / / 4Copy the code

From the above we know that RHS references to B cannot be done inside the function because b is not defined inside the function, but in this case we can do it in the upper scope (in this case, the global scope).

The lookup rule is simple: the engine looks for a variable from the current execution scope, and if it doesn’t find it, the search continues as if it were up one level, and when it reaches the outermost global scope, the search stops whether it was found or not. So a top-down search relationship is a chain search relationship.

So what happens if you don’t find it? When making an RHS reference, the engine throws a ReferenceError exception if the RHS queries all nested scopes and does not find the desired variable.

By contrast, when the engine executes an LHS query, if the target variable cannot be found in the global scope, a variable with that name is created in the global scope and returned to the engine. The premise is that the program is running in non-strict mode. Otherwise, ReferenceError will be raised.

So during code writing, ReferenceError is associated with scope-check failure, while TypeError represents scope-check success, but the operation on the result is illegal.

conclusion

So, having written this, you have a clear understanding of what scopes are all about? Ok, let me try to restate what scope is:

Scope is a set of “query rules for identifiers” (note that I use the word rules here) that perform LHS and RHS queries based on the purpose of the lookup. Determines where (current scope, parent scope… Global scope) how to find (LHS, RHS).

Of course, this article is also posted on my blog, JavaScript Scope Again, if you are interested.

reference

Flanagan. JavaScript Authoritative Guide [M]. Beijing: China Machine Press, 2012. JavaScript you don’t know [M]. Beijing: Posts and Telecommunications Press, 2015.