js basic carding-how to understand scope and scope chain?

Keywords: Javascript Attribute Programming JQuery

This article focuses on the establishment of scoping chains in the lifecycle of the execution context. Before that, review some knowledge about scoping.

1. What is scope?

In JavaScritp Advanced Programming, there is no exact definition of scope, but the concept of execution context is simply stated in "4.2 execution environment and scope". And the execution environment is actually before. Blog: js basic carding - what exactly is execution context stack (execution stack), execution context (executable code)? Execution context in.

In the JavaScript Authoritative Guide, scope is described as:

Variable scope: The scope of a variable is the area in the source code that defines the variable.

Scope is described in Javascript I You Don't Know:

Responsible for collecting and maintaining a series of queries consisting of all life identifiers (variables), and implementing a very strict set of rules to determine the access rights of currently executed code to these identifiers.

Simply speaking, scope is the effective scope of variable access rules.

  • Outside the scope, variables within the scope cannot be referenced.
  • After leaving the scope, the memory space of the variables in the scope is cleared, such as executing the function or closing the browser.
  • Scope and execution context are two completely different concepts. I've confused them before, but I have to distinguish them carefully.

The whole execution process of JavaScript code is divided into two stages, code compilation stage and code execution stage. The compilation phase is completed by the compiler, which translates the code into executable code. The scope rules of this phase are determined. The execution phase is completed by the engine. The main task is to execute the executable code, and the execution context is created in this phase.

In a profound way, in fact, the above paragraph focuses on distinguishing function scopes from function execution contexts. Function scopes are defined when a function is declared, and function execution context is created when a function is called. If a function is called multiple times, it creates multiple function execution contexts, but the scope of the function obviously does not change with the number of times the function is called.

1.1 Global Scope

var foo = 'foo';
console.log(window.foo);   // => 'foo' 

Declare a variable in a browser environment, which by default becomes an attribute under a window object.

function foo() {
    name = "bar"
}
foo();
console.log(window.name) // bar

In a function, if a variable is not declared with var, it will be declared as a global variable by default, and if it is in strict mode, it will report an error.

Global variables can cause naming contamination. If the same global variable is operated on multiple places, the definition of global variables will be overwritten for a long time. At the same time, there are too many global variables, which is very inconvenient to manage.

This is why jquery establishes (variables) globally and the rest of the private method attributes are hung under .

1.2 Function Scope

If a local variable is defined in a function, it can only be accessed in the scope of the function.

function doSomething () {
    var thing = 'To have breakfast';
}
console.log(thing); // Uncaught ReferenceError: thing is not defined

Nested function scope:

function outer () {
    var thing = 'To have breakfast';
    function inner () {
        console.log(thing);
    }
    inner();
}

outer();  // To have breakfast

In the outer function, an inner function is nested, so the inner function can access the variables in the outer function upwards.

Since the inner function can access the variables of the outer function, what happens if the inner function is return ed?

function outer () {
    var thing = 'To have breakfast';
    
    function inner () {
        console.log(thing);
    }
    
    return inner;
}

var foo = outer();
foo();  // To have breakfast

As mentioned earlier, when a function is executed, variables in the scope of the function are garbage collected. This code shows that when an internal function that accesses an external function variable is returned, the variables of the external function are saved.

This is when the function where the variable exists has been executed, but the way to throw the function that can be accessed again is "closure". Closures will continue to be sorted out later.

1.3 Block-level Scope

There is a saying in many books that javascript has no concept of block-level scopes. The so-called block scope is the {} wrapped area. But after ES6 came out, this sentence was not so correct. Because let or const can be used to declare variables or constants of a block-level scope.

For example:

for (let i = 0; i < 10; i++) {
    // ...
}
console.log(i); // Uncaught ReferenceError: i is not defined

Discovering this example will give you the same error message as the first example in the scope of a function. Because variable i can only be accessed in the {} block-level scope of the for loop.

Diffusion thinking:

When exactly should let be used? When should const be used?

const is used by default, let is used only when it is really necessary to change the value of the variable. Because the values of most variables should not be changed after initialization, and unexpected changes to variables are the source of many bug s.

1.4 Lexical Scope

Lexical scope can also be called static scope. This means that wherever a function is called, the lexical scope is determined only by where the function is declared.
Since there are static scopes, there are also dynamic scopes.
The scope of dynamic scope is determined by the location of execution when the function is called.

var a = 123;
function fn1 () {
    console.log(a);
}
function fn2 () {
    var a = 456;
    fn1();
}
fn2();   // 123

The above code, the final output result a value, from the fn1 declaration at the location of access to a value 123.
So the scope of JS is static scope, also called lexical scope.

The above 1.1-1.3 can be seen as the type of scope. But this section, in fact, is different from the above three sections. It does not belong to the scope type. It is only a supplementary explanation of the scope.

2. What is scope chain?

In the JS engine, finding the value of an identifier by an identifier will look up from the current scope until the scope finds the first matching identifier location. It's the scope chain of JS.

var a = 1;
function fn1 () {
    var a = 2;
    function fn2 () {
        var a = 3;
        console.log(a);
    }
    fn2 ();
}
fn1(); // 3

In the console.log(a) statement, when JS finds the value of a variable identifier, it finds the variable declaration from inside FN2 to outside function. It finds that a variable already exists inside fn2, so it will not continue to search. Then the final result will be printed 3.

3. Scope chain and execution context

Previous Blog: js basic carding - what exactly is execution context stack (execution stack), execution context (executable code)? The life cycle of the execution context is discussed in this section.

3. Life cycle of execution context

3.1 Creation Phase

  • Generate Variable Objects (VO)
  • Scope chain
  • Determine this pointing

3.2 Implementation Phase

  • Assignment of variables
  • Function reference
  • Execute other code

The above has done so many cushions, in fact, the focus is to sort out this section.
Next, the creation and activation of a function are used to explain how scope chains are created and changed.

3.1 Function Creation Phase

As mentioned above, the scope of a function is determined when it is defined.

This is because a function has an internal property [[scope], in which all parent variable objects are saved when the function is created, but note that [[scope]] does not represent a complete scope chain at this time, because it does not include its own scope at the creation stage.

Take a chestnut:

function foo () {
    function bar () {
        ...
    }
}

When a function is created, its respective [[scope]] is:

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

bar.[[scope]] = [
    fooContext.AO,
    globalContext.AO
];

3.2 Function Activation Phase

When a function is activated, it enters the function context and creates VO/AO, which adds the active object to the front end of the scope chain.

At this point, the scope chain of the execution context is named Scope:

Scope = [AO].concat([[scope]]);

At this point, the scope chain has been created.

3.3 Hold a chestnut

In the following example, combining the previous variable objects, active objects and execution context stack, we summarize the creation process of scope chain and variable objects in the context of function execution:

var x = 10;
 
function foo() {
  var y = 20;
 
  function bar() {
    var z = 30;
    console.log(x +  y + z);
  }
 
  bar();
}
 
foo(); // 60

Everyone knows for sure that the print result will be 60. But how does the execution context stack and scope chain of the entire code change from the beginning of the first line to the end of the last line?

// Step 1: Enter the global context, where the execution context stack is like this:
ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    }
];

// Step 2: The foo function is created, at which time the execution context stack remains unchanged, but the scope of the foo function is created, and the scope chain is saved to the internal attribute [[scope].
ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    }
];
foo.[[scope]] = [
    globalContext.VO
];

// Step 3: Execution of foo function, entering the creation stage of foo function context
// At this stage, it does three things:
// 1. Duplicate the previous foo.[[scope]] attributes into the context of foo function, and create the scope chain of foo function;
// 2. Create the variable object in the context of foo function and initialize the variable object, then add the parameter, function declaration and variable declaration in turn.
// 3. Add the variable object in the context of the foo function to the front of the scope chain of the foo function created in the first step.
// Ultimately, after these three steps, the entire execution context stack looks like this

ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        VO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: undefined
        },
        Scope: [foo.VO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.VO,
    globalContext.VO
];

// The fourth step: foo function execution, into the foo function context execution stage.
// At this stage, the following two things have been done:
// 1. Change the variable object VO of foo execution context to the active object AO, and modify the value of variables in AO.
// 2. It is found that a bar function is created, and all the parent variable objects of the bar function are saved on the [[scope]] attribute of the bar function.


ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: 20
        },
        Scope: [foo.AO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.AO,
    globalContext.VO
];

bar.[[scope]] = [
    foo.AO,
    globalContext.VO
];

// The fifth step, the bar function execution, enters the creation stage of the bar function context.
// Similar to step three, three things have been done, except that the subject becomes a bar.
// 1. Replicate the previous bar.[[scope]] attributes into the context of the bar function, and create the scope chain of the foo function;
// 2. Create a variable object in the context of bar function and initialize the variable object, then add the parameter, function declaration and variable declaration in turn.
// 3. Add the variable object in the context of the bar function to the front of the scope chain of the bar function created in the first step.
// Ultimately, after these three steps, the entire execution context stack looks like this

ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: 20
        },
        Scope: [foo.AO, globalContext.VO]
    },
    <bar>functionContext: {
        VO: {
            arguments: {
                length: 0
            },
            z: undefined
        },
        Scope: [bar.VO, foo.AO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.AO,
    globalContext.VO
];

bar.[[scope]] = [
    bar.VO,
    foo.AO,
    globalContext.VO
];

// Step 6: Execution of the bar function, entering the execution stage of the context of the bar function
// Similar to step four. At this point, however, no new function context will be created in the bar function.
// 1. Change the variable object VO of bar execution context to the active object AO, and modify the value of variables in AO.
ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: 20
        },
        Scope: [foo.AO, globalContext.VO]
    },
    <bar>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            z: 30
        },
        Scope: [bar.AO, foo.AO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.AO,
    globalContext.VO
];

bar.[[scope]] = [
    bar.AO,
    foo.AO,
    globalContext.VO
];

// Step 7: Execute console.log(x + y +z) in the bar function to find the three identifiers x,y,z

- "x"
-- <bar>functionContext.AO   // No, go on to foo.AO.
-- <foo>functionContext.AO   // I haven't found it yet. Go to globalContext.VO again.
-- globalContext.VO     // Find it. The value is 10.

- "y"
-- <bar>functionContext.AO   // No, go on to foo.AO.
-- <foo>functionContext.AO   // Find it. The value is 20.

-- "z"
-- <bar>functionContext.AO   // Find it. The value is 30.

//Print result: 60.

// Step 8: After the bar function is executed, it pops up from the execution context stack. After the foo function is executed, it pops up from the execution context stack. Ultimately, the execution context stack, leaving only the globalContext

ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    }
]

Feeling can actually simplify understanding, dividing the third, fourth and fifth and sixth steps into one step.

It may be the consolidation of basic knowledge or the record of learning new knowledge. In the next blog post, I will sort out this pointing problem. If you are also interested, you can also go to collect relevant information, then we will learn and discuss together.

Posted by kuk on Wed, 20 Mar 2019 15:33:27 -0700