scope

The basic function of a programming language is to store a value in a variable and access or modify that value later. Introducing variables into a program raises several questions. Where are the variables stored? How can the program find them when it needs them?

First, compilation principle

1. Compile language classification

A computer cannot understand high-level languages, much less execute them directly. It can only understand machine language (binary code) directly, so if a program written in any high-level language is to be run by a computer, it must be converted into machine language, or machine code. And this kind of transformation way has two kinds: compile, explain. From this high-level language also divides into compile type language and explain type language.

The main difference is that compiled language source programs compile and run on the platform, while interpreted languages compile at run time. So the former is fast and the latter is cross-platform.

1.1 Compiled languages

Using a specialized compiler, the high-level language source code for a particular platform is compiled into machine code that can be executed by the platform’s hardware, and packaged into an executable program format that the platform can recognize.

Advantages:

Fast running speed, high code efficiency, compiled program can not be modified, good confidentiality.

Disadvantages:

  • The code needs to be compiled before it can be run, which has poor portability and can only be run on compatible operating systems.
  • Less secure than interpreted languages, a compiled program can access any area of memory and do whatever it wants to your PC (most viruses are written in compiled languages).

The compilation process

In the flow of a traditional compiled language, a piece of source code in a program goes through three steps, collectively called “compilation,” before it is executed.

  • Lexical analysis: Breaking a string into blocks of code that make sense (to a programming language), called lexical units (tokens)
  • Parsing: Converts a stream of lexical units (arrays) into a tree of nested elements that represent the syntax structure of the program. This Tree is called the Abstract Syntax Tree (AST).
  • Code generation: The process of converting an AST into executable code is called code generation.

1.2 Interpreted language

Use a specialized interpreter to interpret the source program line by line into platform-specific machine code and execute it immediately. The code is dynamically translated and executed by the interpreter line by line at execution time, rather than being translated before execution.

Advantages:

  • Interpreted languages provide excellent debugging support.
  • Interpreters are easier to implement than compilers.
  • Intermediate language code is much smaller than compiled executable code. For example, a C/C++.exe file is much larger than a Java.class file that does the same thing.
  • Good portability, as long as there is an interpretation of the environment, can be run on different operating systems. For example, variable types can be dynamically changed, programs can be modified, and good debugging diagnostics can be inserted into the program during interpretation execution. Porting the interpreter to a different system allows the program to run on the ported interpreter system unchanged.
  • Interpreted languages can also provide a high degree of security-something that Internet applications desperately need

Disadvantages:

  • Execution requires an interpretation environment, and programs are heavily platform dependent.
  • It runs slower than compiled, takes up more resources, and the code is inefficient. As well as allocating space to user programs, the interpreter itself takes up valuable system resources.
  • Interpreted applications are much slower than compiled applications due to their decode-fetch-execute cycle.

2.javascript

Although JavaScript is often categorized as a “dynamic” or “interpreted execution” language, it is in fact a compiled language. However, unlike traditional compiled languages, it is not compiled ahead of time and the compiled results cannot be migrated to distributed systems.

2. Understand scope

Simple to understand: A scope is a set of rules for finding variables by name.

1. Several roles involved in the scope

  • Lead engine is responsible for the compilation and execution of the entire JavaScript program from start to finish
  • The compiler is responsible for parsing and code generation
  • A scope is responsible for collecting and maintaining a series of queries made up of all declared identifiers (variables) and enforcing a very strict set of rules that determine access to these identifiers by currently executing code

Assignment to a variable performs two actions: first, the compiler declares a variable in the current scope (if it hasn’t been declared before), and then at runtime the engine looks for the variable in the scope and assigns it if it can find it.

2. Compiler LHS and RHS

LHS and RHS, “L” and “R” represent the left and right side of an assignment operation, respectively. In other words, an LHS query is performed when the variable appears on the left side of the assignment operation and an RHS query is performed when the variable appears on the right. To be more precise, an RHS query is no different from 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.”

2.1 LHS query

An LHS query refers to finding the container itself for a variable so that it can be assigned. That is, find the target of the assignment operation.

It is important to know that LHS queries along the scope chain, assigns the value to the variable if it is found, and creates the variable at the top of the scope chain if it is not found at the top.

For example, if var a = 2, declare var a in scope and then perform the assignment of a = 2. Here a is an LHS reference, we just want to find a target to assign to 2, we don’t care what the value of the target (a) is. Because var a; A has already been added to the current scope, so when LHS queries a, it finds a, which is the target of the assignment operation. If we don’t have var a; When LHS queries a, it will not find a and will create the variable a in scope.

2.2 RHS query

An RHS query is simply a query for the value of a variable, that is, to get the value of a variable.

RHS queries along the scope chain, retrieves the value if it finds it and returns it, or throws an error (such as TypeError) if it doesn’t find it at the top of the scope.

For example: console.log(a),a is an RHS reference, because console.log needs to get the value of A to output the value of A. Log is also an RHS reference, which performs an RHS query on the console object and checks if the resulting value contains a method called log.

2.3 Differences between LHS and RHS

The important distinction between LHS and RHS is that the two queries behave differently when the variable has not been declared (it cannot be found in any scope) and in subsequent operations.

  • When the engine executes an LHS query, if the target variable cannot be found at the top level (global scope), the global scope creates a variable with that name and returns it to the engine, provided the program is not running in “strict mode.”
  • When the engine executes an RHS query, it throws a ReferenceError if the desired variable cannot be found in all nested scopes. Next, if the RHS query finds a variable and you try to manipulate the value of the variable improperly, such as trying to make a function call on a value of a non-function type, or referring to an attribute in a value of type NULL or undefined, the engine will raise another type of exception called TypeError.

ReferenceError is associated with a scope discrimination failure, while TypeError represents a scope discrimination success, but the operation on the result is illegal or unreasonable. Strict mode: : Strict mode was introduced in ES5. There are a lot of behavioral differences between the strict mode and the normal mode, or the loose/lazy mode. One of the different behaviors is that strict mode disallows automatic or implicit creation of global variables.

2.4 Comprehensive Examples

General programs run with both LHS and RHS references.

(function test() {
    a=2;
});
console.log(a);
Copy the code

In the example above, a=2 of the test method will be queried by LHS, and a will not be found along the scope chain. Here, the top of the scope chain will only enclose the scope of test (). Therefore, if variable A is not found, a variable a is created at the top of the action chain in () enclosing test.

An RHS query on A in the console.log (a) statement looks up the scope chain and cannot find A, so an error is thrown.

(function test() {
    a=2; }) ();console.log(a);
Copy the code

After executing the above code, you will find that no error is thrown, because after the test function is run, LHS queries variable A along the scope, and eventually creates variable A at the top of the scope. So when the console.log (a) RHS query is performed on A, a is found in the global scope by searching down the scope chain, and the value of A is obtained.

In summary, the following code results can be considered

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

3. Scope nesting

In actual program execution, one block or function is often nested within another block or function, and this is where scoping occurs.

The following code:

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

The scope lookup visualization looks like this:

The rule for traversing a nested scope chain is simple: The engine looks for variables starting at the current execution scope, and if it doesn’t find any, it continues up one level. When the outermost global scope is reached, the search process stops whether it is found or not.

Lexical scope

3.1 the lexical

The lexical scope is the scope defined in the lexical phase, the first working phase of the compiler. In other words, the lexical scope is determined by where you write the variable and block scopes when you write the code, so the lexical analyzer keeps the scope the same when it processes the code (most of the time).

function foo (a) {
    var b = a * 2;
    function bar ( c ) {
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2)
Copy the code

As shown in the figure above, the scoped bubbles of each variable are determined by where the corresponding scoped block code is written, and they are contained level by level. The bar bubble in the code is completely contained in the bubble created by Foo, only because that’s where we want to define the function bar.

shelter

Scoped look-ups always start at the innermost scope of the runtime and work step by step outward or upward, stopping when the first matching identifier is found. You can define identifiers of the same name in multiple nested scopes, which is called the “shadowing effect” (the inner identifier “overshadows” the outer identifier).

The global variable

Global variables automatically become properties of global objects (such as window objects in the browser), so they can be accessed indirectly, not directly by their lexical names, but by references to global object properties.

var a = 2 ;
window.a ;
Copy the code

Window. a uses this technique to access global variables that are obscured by variables of the same name. But non-global variables that are obscured cannot be accessed anyway.

Only level 1 identifiers are found

Lexical scoped look-up only looks for first-level identifiers, such as A, B, and C. If foo.bar.baz is referenced in your code, the lexical scoping lookup will only attempt to find the foo identifier, and once this variable is found, the object property access rules will take over access to the bar and baz properties, respectively.

3.2 Deceptive morphology

Although the lexical scope is defined entirely by the location declared by the function during code writing, we can also “modify” (or cheat) the lexical scope at run time. There are two mechanisms in JavaScript to do this.

3.2.1 eval

In the JavaScript eval (..) A function can take a string as an argument and treat its contents as if they existed at that point in the program at the time of writing.

function foo (str, a) {
    eval(str);
    console.log( a, b );
}
var b = 2;
foo(" var b = 3".1) / / 1 3
Copy the code

In the above code example, eval(..) Call “var b = 3;” The code is treated as if it was there anyway, that is, the code is actually in foo(..) Internally, a variable b is created and a variable of the same name in the outer (global) scope is obscured. Since the code declares a new variable B, it makes a call to the already existing foo(..) The lexical scope of the

Eval in strict mode

In strict mode programs, eval(..) At runtime, it has its own lexical scope, meaning that declarations within it cannot be modified to the scope they are in.

function foo(str) {
    "use strict";
    eval( str );        
    console.log( a ); // ReferenceError: a is not defined    
}    
foo( "var a = 2" );
Copy the code

3.2.2 with

With is often used as a shortcut to refer repeatedly to multiple properties in the same object without having to refer repeatedly to the object itself.

var obj = {
    a: 1.b: 2.c: 3,}// repeat reference to obj
obj.a = 4;
obj.b = 5;
obj.c = 6;
with (obj) {
    a = 7;
    b = 8;
    c = 9;
}
Copy the code

With can treat an object with no or multiple attributes as a fully isolated lexical scope, so the attributes of the object are also treated as lexical identifiers defined within that scope. Although the with block can handle an object in lexical scope, normal var declarations within the block are not limited to the block’s scope, but are added to the function scope in which the with block is located.

function foo(obj) {
    with (obj) {
        a = 2; }}let o1 = {
    a: 3
}
let o2 = {
    b: 3
}
foo( o1 );
console.log( o1.a ); / / 2
foo( o2 );console.log( o2.a ); // undefined
console.log( a ); // 2 -- No, a is leaked to the global scope!


let o3 = {
    c: 4
}
with (o3) {
   d = 4
}
console.log(o3.d)  // undefined
console.log(d)  / / 4
Copy the code

In the above sample code,. foo(..) The obj function takes an obj argument, which is an object reference, and executes with(obj) {.. }. When we pass o1 in, the a = 2 assignment finds o1.a and assigns 2 to it, as shown later in console.log(o1.a). When o2 is passed in, O2 does not have an a attribute, so it is not created. O2. A remains undefined. But this has the side effect of creating a global variable a.

When we pass o1 to with, the scope declared by with is o1, and this scope contains an identifier that matches the o1.a attribute. But when we scoped O2, there was no A identifier, so a normal LHS identifier lookup was done. Scope of O2, foo(..) The identifier A was not found in either the scope or the global scope, so when a = 2 is executed, a global variable is automatically created (because it is in non-strict mode).

3.2.3 performance

eval(..) And with modify or create new scopes at run time to trick other lexical scopes defined at write time. The former allows you to evaluate a “code” string containing one or more declarations to modify existing lexical scopes (at run time). The latter essentially creates a new lexical scope (again at run time) by treating a reference to an object as a scope and an attribute of the object as a scope identifier.

The JavaScript engine performs several performance optimizations at compile time. Some of these optimizations rely on being able to statically analyze the code against its lexology and pre-determine where all variables and functions are defined so that identifiers can be found quickly during execution. But if the engine finds eval(..) in the code Or with, which simply assumes that all judgments about the position of identifiers are invalid because eval(..) cannot be explicitly known at the lexical stage. There is no way of knowing what code will be received, how it will modify the scope, and what exactly is passed to the object with uses to create the new lexical scope.

Functional scope and block-level scope

A scope contains a series of “bubble” containers that contain definitions of identifiers (variables, functions). These bubbles are nested within each other and neatly arranged in a honeycomb pattern defined when writing code. What are the structures that can generate bubbles in Javascript?

4.1 Function scope

Function scope means that all variables belonging to a function can be used and reused throughout the scope of the function (in fact, it can also be used in nested scopes). JavaScript has function-based scope, meaning that each function declared creates a scoped bubble for itself. The following code:

function foo (a){
    var b  = 1;
    function bar () {}var c = 3;
}
bar() // ReferenceError
console.log(a, b, c) // ReferenceError
Copy the code

foo(..) The scope bubble contains identifiers A, B, C, and bar. bar(..) Has its own scoped bubble. The global scope also has its own scope bubble, which contains only one identifier: foo. Since identifiers A, B, C, and bar are all attached to foo(..) Scoped bubble, therefore cannot be retrieved from foo(..) External access to them. That is, none of these identifiers are accessible from the global scope, but in foo(..) The interior of the bar(..) is accessible. The interior can also be accessed (assuming bar(..) There is no internal identifier declaration with the same name.

4.1.1 role

The traditional view of a function is to declare a function and then add code to it. But the reverse can also be instructive: taking an arbitrary piece of written code and wrapping it around function declarations effectively “hides” the code. Is hiding variables and functions a useful technique?

Security code

As a principle of minimum security, in software design, you should expose as little as necessary and “hide” everything else, such as the API design of a module or object.

function doSomething(a) {    
    b = a + doSomethingElse( a * 2 );    
    console.log( b * 3 );
}
function doSomethingElse(a) {
    return a - 1;
}
var b;
doSomething( 2 ); / / 15
Copy the code

In the code snippet above, the variable B and the function doSomethingElse(..) Should be to doSomething (..) Internal concrete implementation of “private” content. Give an external scope to b and doSomethingElse(..) Not only are “access rights” unnecessary, but they can be “dangerous” because they can be used intentionally or unintentionally in unexpected ways, resulting in exceeding doSomething(..) Is applicable to. A more “sensible” design would hide these private details in doSomething(..) Internal.

function doSomething(a) {
    function doSomethingElse(a) {
        return a - 1;
    }
    var b;    
    b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
doSomething( 2 ); / / 15
Copy the code

Modified code B and doSomethingElse(..) Cannot be accessed from the outside, but can only be accessed by doSomething(..) The control of. Functionality and final results are not compromised, but the design privates the specifics that all well-designed software implements.

To avoid conflict

Another benefit of hiding variables and functions in scope is to avoid conflicts between identifiers of the same name, two identifiers that may have the same name but serve different purposes, which can inadvertently cause naming conflicts. Collisions can cause the value of a variable to be accidentally overwritten.

function foo() {
    function bar(a) {        
        i = 3; // Modify I in the scope of the for loop
        console.log( a + i );    
    }
    for (var i=0; i<10; i++) {        
        bar( i * 2 ); // Oops, infinite loop!
    }
}
foo();
Copy the code

In the above code example, bar(..) The internal assignment expression I = 3 accidentally overrides the declaration in foo(..) The I in the internal for loop, because I is fixed to 3, always satisfies the condition less than 10, causing an infinite loop. There are two solutions: one is in bar(..) The internal assignment declares a local variable to use, using any name, such as var I = 3; The other is to use a completely different identifier name, such as var j = 3.

4.1.2 Execute function immediately

Adding a wrapper function outside of any code snippet “hides” internal variables and function definitions so that the outer scope cannot access anything inside the wrapper function. But this creates two problems: first, you need to declare a named function that does this, such as foo() in the following example, which causes foo to pollute its scope; Second, the function must be explicitly called by its name (foo()) to run its code.

var a = 2;
function foo() { 
    // <-- add this line
    var a = 3;    
    console.log( a ); / / 3
} 
// <-- and this line
foo();
// <-- and this line
console.log( a ); / / 2
Copy the code

JavaScript provides a solution to both of these problems: functional expressions.

var a = 2;
(function foo(){ // <-- add this line
    var a = 3;    
    console.log( a ); / / 3}) ();// <-- and this line
console.log( a ); / / 2
Copy the code

Compare the two code snippets above. In the first fragment, foo is bound to its scope and can be called directly from foo(). In the second fragment foo is bound to a function within the function expression itself rather than its scope. In other words (function foo(){.. }) as a function expression means that foo can only be used in.. Is accessed from the location represented, but not from the external scope. The foo variable name being hidden within itself means that it does not unnecessarily contaminate the external scope.

The easiest way to distinguish a function declaration from an expression is to look at where the function keyword appears in the declaration (not just in one line of code, but in the entire declaration). If function is the first word in the declaration, it is a function declaration; otherwise, it is a function expression. The most important difference between function declarations and function expressions is where their name identifiers will be bound.

Functions and function expressions

Function expressions can be anonymous, and function declarations cannot omit function names — this is illegal in JavaScript syntax. The most common anonymous function expressions are callback arguments;

setTimeout( function() {    
    console.log("I waited 1 second!");
}, 1000 );
Copy the code

Disadvantages of anonymous functions:

  • Anonymous functions do not show meaningful function names in the stack trace, making debugging difficult
  • Without a function name, the expired arguments.callee reference can only be used when the function needs to reference itself, such as in recursion. Another example of a function needing to reference itself is when the event listener needs to unbind itself after the event is fired
  • Anonymous functions omit function names that are important to code readability/understandability. A descriptive name lets the code speak for itself.

It is a best practice to always name function expressions:

setTimeout( function timeoutHandler() {   // <-- look, I have a name!
    console.log( "I waited 1 second!" );
}, 1000 );
Copy the code

Immediately Invoked Function Expression

The function is enclosed within a pair of () parentheses, and thus becomes an expression that can be executed immediately by adding another () to the end, indicating immediate execution of the function expression.

var a = 2; 
(function IIFE() {
    var a = 3;    
    console.log( a ); / / 3}) ();console.log( a ); / / 2
Copy the code

There is an improved form that many prefer to the traditional IIFE form :(function(){.. }()) The two forms are functionally identical. Which one to choose is a matter of personal preference.

var a = 2; 
(function IIFE() {
    var a = 3;    
    console.log( a ); / / 3} ());console.log( a ); / / 2
Copy the code

Another very common advanced use of IIFE is to call them as functions and pass in arguments.

var a = 2;
(function IIFE( global ) {
    var a = 3;    
    console.log( a ); / / 3
    console.log( global.a ); / / 2}) (window );
console.log( a ); / / 2
Copy the code

Another variation of IIFE is to invert the running order of the code, placing the function that needs to be run in the second place and passing it in as an argument after IIFE execution.

var a = 2;
(function IIFE( def ) {    
    def( window); }) (function def( global ) {
        var a = 3;    
        console.log( a ); / / 3
        console.log( global.a ); / / 2});Copy the code

The function expression def is defined in the second part of the fragment, and then passed as a parameter (also called def) into the first part of the IIFE function definition. Finally, the def argument (that is, the function passed in) is called, passing window as the value of the global argument.

4.2 Block-level scope

JavaScript also has block scopes in addition to function scopes. Block scope is a tool used to extend the previous principle of minimum authorization by extending code from hiding information in functions to hiding information in blocks.

Block-level scope in the for loop

for (var i=0; i<10; i++) {    
    console.log( i );
}
Copy the code

Block-level scope in with

Scopes created from objects with with are valid only in the with declaration, not in the outer scope.

Block level scope created by try/catch

try {
    undefined(a);// Perform an illegal operation to force an exception
} catch (err) {    
    console.log( err ); // Can be executed properly!
}
console.log( err ); // ReferenceError: err not found
Copy the code

The let keyword defines declared variables

The let keyword binds variables to any scope they are in (usually {.. } inside). In other words, the variables declared by the let are implicitly appended to the block scope. Let and var:

var flag = true;
if(flag){
    var flaga = 11;
}
console.log(flaga) / / 11
if(flag){
    let flagb = 22;
}
console.log(flagb) // flagb is not defined
Copy the code

The const keyword

Const, can also be used to create block-scoped variables, but the value is fixed (constant). Any subsequent attempts to modify the value will cause.

var foo = true;
if (foo) {
    var a = 2;
    const b = 3; // Block scoped constants contained in if
    a = 3; / / normal!
    b = 4; / / error!
}
console.log( a ); / / 3
console.log( b ); // ReferenceError!
Copy the code

Summary of notes from Javascript you Don’t Know

If you have any questions, welcome to discuss, if you are satisfied, please manually click “like”, thank you! 🙏

For more postures, please pay attention!!